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 order below is the documented one, and
further down you can see it logged, method by method, on .NET 10.
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."
ShouldRender — the gate before every re-render
After the first render, Blazor asks this method before each further render. Return
false to say "nothing on screen changed, skip the work." You rarely need it, but it is
the one knob for a component that re-renders too often. It is never asked before the first render,
which the log below confirms.
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.
One rule to remember: it only runs when the component is interactive. A statically rendered
page, and the prerender pass of an interactive one, never call it, because there is no live browser
DOM to talk to.
Dispose — the component leaves
When the user navigates away, or a parent stops rendering the child, the component is removed, and if
it implements IDisposable its Dispose runs (IAsyncDisposable
and DisposeAsync for async cleanup). This is where a timer is stopped and an event
subscription unhooked, so a dead component does not keep ticking in the background. Add
@implements IDisposable at the top of the file to use it.
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.
What actually ran: a probe component on .NET 10
Everything above is the documented order. To check it, we built a component that overrides every
lifecycle method and reports each call to a tiny endpoint: the method, the renderer it ran on, the
firstRender flag, the value of a Counter parameter and an instance id. Then
we loaded it under each render mode and clicked. The probe and every raw log are linked at the end.
Here is the log of an InteractiveServer page on first load (ASP.NET Core 10.0.12):
01 | SetParametersAsync | firstRender=- | Counter=0 | Renderer=Static | IsInteractive=False | IsBrowser=False | Instance=3961942a
02 | OnInitialized | firstRender=- | Counter=0 | Renderer=Static | IsInteractive=False | IsBrowser=False | Instance=3961942a
03 | OnInitializedAsync | firstRender=- | Counter=0 | Renderer=Static | IsInteractive=False | IsBrowser=False | Instance=3961942a
04 | OnParametersSet | firstRender=- | Counter=0 | Renderer=Static | IsInteractive=False | IsBrowser=False | Instance=3961942a
05 | OnParametersSetAsync | firstRender=- | Counter=0 | Renderer=Static | IsInteractive=False | IsBrowser=False | Instance=3961942a
06 | Dispose | firstRender=- | Counter=0 | Renderer=Static | IsInteractive=False | IsBrowser=False | Instance=3961942a
07 | SetParametersAsync | firstRender=- | Counter=0 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
08 | OnInitialized | firstRender=- | Counter=0 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
09 | OnInitializedAsync | firstRender=- | Counter=0 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
10 | OnParametersSet | firstRender=- | Counter=0 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
11 | OnParametersSetAsync | firstRender=- | Counter=0 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
12 | OnAfterRender | firstRender=True | Counter=0 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
13 | OnAfterRenderAsync | firstRender=True | Counter=0 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
Two instances. The first, Renderer=Static, is the prerender: it runs the five
methods from SetParametersAsync to OnParametersSetAsync, is disposed, and
never sees OnAfterRender. Then the circuit starts, a brand-new instance runs the same five
methods again and, because it is interactive, OnAfterRender and
OnAfterRenderAsync with firstRender=True. That is the documented
"OnInitializedAsync runs twice when prerendering", caught in the act: the data you load
there is loaded twice unless you persist it between the two passes.
Then one click on the parent's Increment button, followed by a click on a link to another page:
01 | SetParametersAsync | firstRender=- | Counter=0 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
02 | OnParametersSet | firstRender=- | Counter=1 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
03 | OnParametersSetAsync | firstRender=- | Counter=1 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
04 | ShouldRender | firstRender=- | Counter=1 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
05 | OnAfterRender | firstRender=False | Counter=1 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
06 | OnAfterRenderAsync | firstRender=False | Counter=1 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
07 | Dispose | firstRender=- | Counter=1 | Renderer=Server | IsInteractive=True | IsBrowser=False | Instance=a8bf472a
No OnInitialized: the same instance (a8bf472a) is reused. The parameters
arrive (the probe logs SetParametersAsync before the base method assigns them, which is
why that line still says 0), OnParametersSet sees the new value, ShouldRender
is asked for the first time, the component renders, and OnAfterRender reports
firstRender=False. Leaving the page runs Dispose and nothing else.
Render modes change the count, not the order
- Static SSR (a page with no render mode) is the prerender half on its own: the
five methods and
Dispose, six lines, noShouldRender, noOnAfterRender. There is no button to click, because nothing is interactive. - InteractiveServer with
prerender: falseis the interactive half on its own: one instance, seven lines,OnInitializedonce. - InteractiveWebAssembly logged exactly the same thirteen lines as InteractiveServer,
except that the second instance reports
Renderer=WebAssemblyandIsBrowser=True: the same component code ran in the browser.
The method reference
| Method | When it runs | Static SSR and prerender | Typical use |
|---|---|---|---|
SetParametersAsync | First, every time parameters arrive | Yes | Rarely overridden; always call the base method |
OnInitialized / Async | Once per instance, after the first parameters | Yes, and again on the interactive instance | Load data once |
OnParametersSet / Async | After initialization, and whenever the parent re-renders with parameters | Yes | React to a changed parameter |
ShouldRender | Before every re-render, never the first | No | Skip a pointless render |
OnAfterRender / Async | After the DOM is updated; firstRender is true once | No | JavaScript interop, focus, measuring |
Dispose / DisposeAsync | When the component is removed | Yes, after the response | Stop timers, unhook events, cancel work |
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. - Calling JavaScript in
OnInitializedAsync. During prerendering there is no browser yet, so the call fails with "JavaScript interop calls cannot be issued at this time". Move it toOnAfterRenderAsyncwithfirstRender.
Note: This is settled knowledge — the logs above come from .NET 10.0.12 and match
the documented order — 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.
Do it yourself
The probe is small enough to drop into any Blazor Web App (dotnet new blazor -int Auto).
Files are served as plain text:
- LifecycleProbe.razor, the component that reports every method
- LifecycleLog.cs, the helper that posts one line per call, and Program.cs with the three-line log endpoint
- ProbeServer.razor, the parent page with the Increment button and the Leave link
- Raw logs: static SSR, InteractiveServer load, InteractiveServer increment and leave, InteractiveServer without prerender, WebAssembly load, WebAssembly increment and leave
Load each probe page, click Increment, click Leave, and read /api/log. Then add an
await Task.Delay(500) inside OnInitializedAsync and watch the extra render
that an awaited task causes, which these logs deliberately leave out.
Frequently asked
- In what order do Blazor lifecycle methods run?
- SetParametersAsync first, then OnInitialized and OnInitializedAsync once per instance, then OnParametersSet and OnParametersSetAsync, then the render, then OnAfterRender and OnAfterRenderAsync with firstRender true the first time. On every later change the order is SetParametersAsync, OnParametersSet, ShouldRender, render, OnAfterRender with firstRender false. Dispose runs when the component is removed. This is the order logged on ASP.NET Core 10.0.12.
- Why does OnInitializedAsync run twice in Blazor?
- Because of prerendering. An interactive page is first rendered statically on the server to produce HTML, and that pass creates one component instance which runs OnInitialized and OnInitializedAsync and is then disposed. When the circuit or the WebAssembly runtime starts, a second instance runs them again. Disable prerendering with prerender: false, or persist the loaded data between the two passes, to avoid loading twice.
- Does OnAfterRender run in static server-side rendering?
- No. OnAfterRender and OnAfterRenderAsync run only when the component is interactive, after the browser DOM has been updated. A statically rendered page, and the prerender pass of an interactive page, run SetParametersAsync, OnInitialized, OnParametersSet and then Dispose, and never call OnAfterRender or ShouldRender.
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!