Every Blazor component moves through a predictable lifetime: it is created, its parameters are set, it renders, it may re-render many times as state changes, and eventually it is removed from the page. The runtime raises lifecycle events at each transition, and your job is to put code in the right event — not just the most convenient one.

Putting code in the wrong lifecycle method is the most common source of hard-to-reproduce bugs in Blazor: data that loads twice, timers that keep running after the component is gone, and UI that flickers on first render.

The Full Lifecycle Sequence

Order Method When it runs What belongs here
1 SetParametersAsync Before any lifecycle method, every render Rarely override — framework manages it
2 OnInitialized Once, after first parameters are set Synchronous setup, default state
3 OnInitializedAsync Once, immediately after OnInitialized Async data fetching (API, database)
4 OnParametersSet Every time parent re-renders or parameters change Reacting to parameter value changes
5 OnParametersSetAsync After OnParametersSet, every parameters change Async work triggered by parameter changes
6 OnAfterRender After every render (including re-renders) JS interop, DOM measurement
7 Dispose When component is removed from the tree Cancel subscriptions, stop timers

OnInitialized — Synchronous Setup

Runs once when the component is first created, before the first render. Use it for anything that does not need to await — setting default values, building lookup dictionaries from already-loaded data, subscribing to synchronous events.

protected override void OnInitialized()
{
    // Runs synchronously before first render.
    pageTitle   = "Product Catalogue";
    sortColumn  = "Name";
    sortAscending = true;
}

OnInitializedAsync — The Correct Home for Data Fetching

This is where HTTP calls and database queries belong. Blazor will render the component twice: once immediately (so you can show a loading state) and again after the Task completes.

private bool   isLoading = true;
private List<Product> products = new();
private string errorMessage = "";

protected override async Task OnInitializedAsync()
{
    try
    {
        isLoading = true;
        products  = await ProductService.GetAllAsync();
    }
    catch (Exception ex)
    {
        errorMessage = $"Failed to load: {ex.Message}";
    }
    finally
    {
        isLoading = false;
    }
}
@if (isLoading)
{
    <p>Loading products…</p>
}
else if (!string.IsNullOrEmpty(errorMessage))
{
    <p class="error">@errorMessage</p>
}
else
{
    @foreach (var p in products)
    {
        <div>@p.Name — $@p.Price</div>
    }
}

OnParametersSet — Reacting to Parameter Changes

Called every time the parent re-renders and passes new (or the same) parameters. Use it to derive local state from parameter values — for example, loading detail data when a selected ID changes.

// Parent passes: <ProductDetail ProductId="@selectedId" />
[Parameter] public int ProductId { get; set; }

private Product? currentProduct;
private int      _lastProductId;

protected override async Task OnParametersSetAsync()
{
    // Guard: only reload when the ID actually changes.
    if (ProductId == _lastProductId) return;

    _lastProductId   = ProductId;
    currentProduct   = await ProductService.GetByIdAsync(ProductId);
}

Without the guard, every time the parent re-renders (even for unrelated reasons) you would fire a new HTTP request.

OnAfterRender — The DOM Exists Here

Blazor renders to a virtual tree; the real DOM only exists after the render is committed. OnAfterRender is the only lifecycle event where it is safe to call JavaScript interop that reads DOM properties like scrollHeight or getBoundingClientRect.

@inject IJSRuntime JS

private bool _focusApplied = false;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    // firstRender is true only on the very first render.
    if (firstRender)
    {
        await JS.InvokeVoidAsync("focusElement", "search-input");
        _focusApplied = true;
    }
}

Always guard with firstRender unless you genuinely need to run on every re-render. Running JS interop on every render is a common performance trap.

IDisposable.Dispose — Prevent Silent Memory Leaks

If your component subscribes to events, registers callbacks, or starts timers, you must implement IDisposable and clean up in Dispose. Failing to do so means the garbage collector cannot collect the component, and the callbacks fire on a dead object.

@implements IDisposable

@code {
    private System.Timers.Timer? _refreshTimer;

    protected override void OnInitialized()
    {
        _refreshTimer          = new System.Timers.Timer(5000);
        _refreshTimer.Elapsed += OnTimerElapsed;
        _refreshTimer.Start();
    }

    private async void OnTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
    {
        // Timer fires on a thread-pool thread — must marshal back.
        await InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        _refreshTimer?.Stop();
        _refreshTimer?.Dispose();
    }
}

The InvokeAsync(StateHasChanged) Pattern

Blazor components run on a single synchronisation context. External callbacks — timer events, SignalR messages, Task continuations — arrive on thread-pool threads. Calling StateHasChanged() directly from such a thread will throw or silently fail. Wrap it:

// Safe from any thread:
await InvokeAsync(StateHasChanged);

This marshals the call back to the component's synchronisation context before triggering a re-render.

Key Takeaways

  • OnInitialized is for synchronous setup; OnInitializedAsync is for HTTP calls and async data loading.
  • OnParametersSet fires on every parent re-render — always guard with a comparison to avoid redundant work.
  • OnAfterRender(firstRender) is the only safe place for JavaScript interop that touches the real DOM.
  • Any component that registers an external callback or starts a timer must implement IDisposable.
  • From timer callbacks or background threads, always use await InvokeAsync(StateHasChanged) to trigger a safe re-render.