Previously, we moved ClinicApp's patient list into a PatientService and learned how
dependency injection hands it to any
component that asks. Today we answer a question that has quietly followed us since Part 1:
when does a component actually run its code?
The mystery: where do I load my data?
Imagine ClinicApp no longer keeps patients in memory but fetches them from a database or a web API. That fetch takes time. So… where does the fetching code go? In the constructor? Somewhere in the markup? The answer is that every Blazor component lives through a lifecycle — a fixed sequence of stages, each with its own job — and Blazor gives you a method to override at each stage. Once you know the sequence, "where do I put this code?" almost answers itself.
Think of it like a patient visiting the clinic: check-in, initial consultation, follow-up visits, and the moment they finally walk out the door into the waiting room. Let's walk the stages in order.
SetParametersAsync — the parameters arrive
Before anything else, Blazor delivers the component's parameters — the values a
parent passes in, like the Patient we hand to PatientCard, or the
Id that PatientDetails reads from the URL. You will almost never override
this method yourself; just know it runs first, so by the next stage your parameters are ready to use.
OnInitialized and OnInitializedAsync — once per component life
This pair runs once, right after the component is created and its first parameters
are set. It is the place to load initial data. Need to call a service? Fetch from an API?
Do it in OnInitializedAsync. The Async version exists so you can
await slow work without freezing anything — if await still feels fuzzy,
our async/await explainer has your back.
OnParametersSet and OnParametersSetAsync — every time parameters change
Here's a subtle one. When you navigate from /patients/3 to /patients/4,
Blazor doesn't throw away your PatientDetails component and build a new one — it
reuses it and simply hands it the new Id. That means
OnInitializedAsync does not run again! If you loaded patient 3 there,
you'd be staring at patient 3 forever. OnParametersSetAsync runs every time parameters
change, so it's the right home for "re-load whatever depends on my parameters."
Render — Blazor draws the component
Next, Blazor turns your markup into UI. You don't write code for this stage — it just happens, automatically, whenever Blazor believes something on screen may have changed.
OnAfterRender and OnAfterRenderAsync — the DOM exists now
Finally, after the component has been drawn, this pair fires. Its superpower is that the actual
HTML elements exist in the browser at this point, which matters mostly for JavaScript-ish work:
focusing an input, measuring an element, starting a chart library. It's the rarest one to need —
a handy first parameter, firstRender, tells you whether this is the very first paint.
The loading-state pattern
Now let's cash in that knowledge. Slow data plus instant rendering means there's a moment where the
page is visible but the data isn't there yet. The fix is a simple, beloved pattern: a
loading flag.
@page "/patients"
@rendermode InteractiveServer
@inject PatientService Patients
<h2>Our patients</h2>
@if (loading)
{
<p>Loading patients…</p>
}
else
{
@foreach (var patient in patients)
{
<PatientCard Patient="patient" />
}
}
@code {
private List<Patient> patients = [];
private bool loading = true;
protected override async Task OnInitializedAsync()
{
patients = await Patients.GetPatientsAsync();
loading = false;
}
}
The flow: the component initializes, loading starts as true, and Blazor
renders the "Loading…" message immediately. When the awaited call finishes, we flip
loading to false, Blazor re-renders, and the real list appears. Your users
see instant feedback instead of a blank stare.
Tip: This little pattern — bool loading = true;, load in
OnInitializedAsync, flip the flag — appears in almost every real Blazor app you
will ever read. Learn it once, spot it everywhere.
One honest paragraph about StateHasChanged
You may have seen StateHasChanged() in other tutorials and wondered if you should be
sprinkling it everywhere. You shouldn't. Blazor automatically re-renders after the things it can
see: button clicks, form input, lifecycle methods finishing. You only need to call
StateHasChanged when data changes outside that flow — say, a timer ticks every
second, or a background task pushes an update. In those cases you're tapping Blazor on the shoulder:
"hey, something changed, please redraw." That's all it is.
Common mistakes to dodge
- Loading data in the constructor. Constructors can't
await, and parameters haven't arrived yet. UseOnInitializedAsyncinstead. - Mixing up the Async suffix duo. Writing awaited work inside
OnInitialized(the sync one) won't compile the way you hope — and definingOnInitializedAsyncwithoutoverridecompiles fine but silently never runs. Double-check both the suffix and theoverridekeyword. - Calling
StateHasChangedinOnAfterRender. That triggers a re-render, which triggersOnAfterRenderagain, which… congratulations, you've built an infinite loop.
Note: This is settled knowledge — the lifecycle works exactly the same in
current Blazor (.NET 10) — and you don't need to memorize every stage today. Remember two:
OnInitializedAsync for "load my data once," and OnParametersSetAsync
for "my parameters changed, react." Those two cover the vast majority of real-world needs.
ClinicApp now knows exactly when to load its data. Next up, we'll give it something real
to load: in Part 11 we call an actual web API over the
internet with HttpClient — and the loading pattern you just learned will be doing the
heavy lifting. See you there!