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.

SetParametersAsync parameters arrive OnInitialized(Async) first time only: load data here once per instance OnParametersSet(Async) every time parameters change ShouldRender re-renders only, never the first return false to skip Render markup becomes HTML OnAfterRender(Async) interactive only: firstRender once the DOM exists now Dispose the component leaves new parameters or StateHasChanged loop back static SSR and prerender stop here: no ShouldRender, no OnAfterRender, then Dispose
What runs when: OnInitialized greets each instance once, OnParametersSet and Render repeat as things change, OnAfterRender only runs when the component is interactive, and Dispose closes the door.

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, no ShouldRender, no OnAfterRender. There is no button to click, because nothing is interactive.
  • InteractiveServer with prerender: false is the interactive half on its own: one instance, seven lines, OnInitialized once.
  • InteractiveWebAssembly logged exactly the same thirteen lines as InteractiveServer, except that the second instance reports Renderer=WebAssembly and IsBrowser=True: the same component code ran in the browser.

The method reference

MethodWhen it runsStatic SSR and prerenderTypical use
SetParametersAsyncFirst, every time parameters arriveYesRarely overridden; always call the base method
OnInitialized / AsyncOnce per instance, after the first parametersYes, and again on the interactive instanceLoad data once
OnParametersSet / AsyncAfter initialization, and whenever the parent re-renders with parametersYesReact to a changed parameter
ShouldRenderBefore every re-render, never the firstNoSkip a pointless render
OnAfterRender / AsyncAfter the DOM is updated; firstRender is true onceNoJavaScript interop, focus, measuring
Dispose / DisposeAsyncWhen the component is removedYes, after the responseStop timers, unhook events, cancel work

Common mistakes to dodge

  • Loading data in the constructor. Constructors can't await, and parameters haven't arrived yet. Use OnInitializedAsync instead.
  • Mixing up the Async suffix duo. Writing awaited work inside OnInitialized (the sync one) won't compile the way you hope — and defining OnInitializedAsync without override compiles fine but silently never runs. Double-check both the suffix and the override keyword.
  • Calling StateHasChanged in OnAfterRender. That triggers a re-render, which triggers OnAfterRender again, 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 to OnAfterRenderAsync with firstRender.

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:

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!