Skip to content

Component Lifecycle & State

Key Points

  • Lifecycle hooks: SetParametersAsyncOnInitialized[Async]OnParametersSet[Async] → render → OnAfterRender[Async](firstRender). Dispose on tear-down.
  • OnInitialized runs once per component instance; OnParametersSet runs whenever parent changes parameters.
  • OnAfterRender(firstRender: true) is the only place to safely call IJSRuntimeDOM is ready.
  • State: cascading values (read-only ambient), state container (DI'd singleton), PersistentComponentState (cross-render-mode).
  • StateHasChanged queues a re-render. ShouldRender lets you skip.

Concepts (deep dive)

Lifecycle order

SetParametersAsync(params)         ← parameters about to change
OnInitialized[Async]                ← first time only
OnParametersSet[Async]              ← every parameter change
[render]
OnAfterRender[Async](firstRender)   ← DOM updated; JS interop OK
public partial class MyComponent : ComponentBase, IAsyncDisposable
{
    [Parameter] public int Id { get; set; }

    protected override async Task OnInitializedAsync()
    {
        // Once per instance. Load initial data.
        _data = await Service.GetAsync(Id);
    }

    protected override async Task OnParametersSetAsync()
    {
        // Each time Id changes (parent re-renders with new value).
        if (Id != _lastId)
        {
            _data = await Service.GetAsync(Id);
            _lastId = Id;
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync("initChart", _chartRef);
        }
    }

    public ValueTask DisposeAsync()
    {
        // Clean up subscriptions, JS handles
        return _chartObj?.DisposeAsync() ?? ValueTask.CompletedTask;
    }
}

SetParametersAsync (advanced)

Default implementation calls the others. Override for ultra-fine control or to skip default param assignment.

ShouldRender

private int _lastRenderedId = -1;

protected override bool ShouldRender()
{
    return Id != _lastRenderedId;
}

protected override void OnAfterRender(bool firstRender)
{
    _lastRenderedId = Id;
}

Skip re-renders for performance.

StateHasChanged

private async Task LoadAsync()
{
    _loading = true;
    StateHasChanged();   // re-render now (show spinner)
    _data = await Service.GetAsync();
    _loading = false;
    // automatic re-render after async return
}

Force re-render. Often unnecessary — Blazor auto re-renders after event handlers and async returns to render context.

InvokeAsync(StateHasChanged)

When you re-render from a non-render context (timer callback, background task):

private void OnTimer(object? state)
{
    InvokeAsync(StateHasChanged);   // marshal to render thread
}

Parameters and EventCallbacks

@code {
    [Parameter] public int Count { get; set; }
    [Parameter] public EventCallback<int> CountChanged { get; set; }
}
<Counter Count="@_count" CountChanged="@(c => _count = c)" />

EventCallback auto-StateHasChanged the parent.

Cascading values

<CascadingValue Value="@_theme">
    <ChildComponent />
</CascadingValue>

<!-- in child -->
@code { [CascadingParameter] public Theme? Theme { get; set; } }

Ambient read-only value. For broader state, use a service.

State container pattern

public class CartState
{
    private readonly List<Item> _items = new();
    public IReadOnlyList<Item> Items => _items;
    public event Action? OnChange;

    public void Add(Item i) { _items.Add(i); OnChange?.Invoke(); }
}

builder.Services.AddScoped<CartState>();
@inject CartState State
@implements IDisposable

<p>Items: @State.Items.Count</p>

@code {
    protected override void OnInitialized() => State.OnChange += Update;
    private void Update() => InvokeAsync(StateHasChanged);
    public void Dispose() => State.OnChange -= Update;
}

PersistentComponentState (Auto/prerender)

[Inject] PersistentComponentState State { get; set; } = default!;
private PersistingComponentStateSubscription _sub;

protected override async Task OnInitializedAsync()
{
    if (State.TryTakeFromJson<MyData>("data", out var saved) && saved is not null)
        _data = saved;
    else
        _data = await Service.GetAsync();

    _sub = State.RegisterOnPersisting(() =>
    {
        State.PersistAsJson("data", _data);
        return Task.CompletedTask;
    });
}

public void Dispose() => _sub.Dispose();

Server prerender persists; WASM picks up. Avoids double-fetch.

Disposal

@implements IAsyncDisposable

public async ValueTask DisposeAsync()
{
    if (_jsObj is not null) await _jsObj.DisposeAsync();
    _cts?.Cancel();
    _cts?.Dispose();
}

Especially important for: - JS interop references. - Event subscriptions. - CancellationTokenSources.

Render tree diffing

Blazor compares previous and current render output; updates only changed parts. Don't manually manipulate DOM (JS interop is the escape hatch).

Async lifecycle gotchas

protected override async Task OnInitializedAsync()
{
    _data = await Service.GetAsync();
}

Render happens with _data == null during the await. Show spinner; check null in razor.

@if (_data is null) { <p>Loading...</p> }
else { /* show */ }

Two-way binding

<input @bind="value" />
<input @bind="value" @bind:event="oninput" />

@bind:event="oninput" updates on every keystroke (default is onchange).

Component teardown

When parent removes component, Dispose runs. Clean subscribers / JS resources.

Capture references

<ChildComponent @ref="_child" />

@code { ChildComponent? _child; }

Use sparingly — implies tight coupling.


Code: correct vs wrong

❌ Wrong: JS interop in OnInitialized

protected override async Task OnInitializedAsync()
{
    await JS.InvokeAsync<string>("eval", "document.title");   // DOM not ready
}

✅ Correct: OnAfterRender(firstRender)

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
        await JS.InvokeAsync<string>("eval", "document.title");
}

❌ Wrong: forgetting Dispose subscription

State.OnChange += Update;   // never -= → memory leak

✅ Correct: Dispose

public void Dispose() => State.OnChange -= Update;

Design patterns for this topic

Pattern 1 — "Service + event for shared state"

  • Intent: decoupled state; subscribe pattern.

Pattern 2 — "PersistentComponentState across modes"

  • Intent: avoid double-fetch.

Pattern 3 — "ShouldRender for perf"

  • Intent: skip unnecessary diffs.

Pattern 4 — "JS interop only in OnAfterRender"

  • Intent: DOM ready.

Pattern 5 — "Async with loading state"

  • Intent: smooth UX.

Pros & cons / trade-offs

Pattern Pros Cons
Service + event Centralized Manual subscriptions
Cascading Easy Read-only
Component params Explicit Drilling
PersistentState Cross-mode Boilerplate

When to use / when to avoid

  • Use OnAfterRender(firstRender) for JS init.
  • Use state container for cross-component state.
  • Avoid JS interop in OnInitialized.
  • Avoid state via static — gets messy.

Interview Q&A

Q1. Lifecycle order? SetParametersAsync → OnInitialized → OnParametersSet → render → OnAfterRender → Dispose.

Q2. OnInitialized vs OnParametersSet? OnInitialized: once per instance. OnParametersSet: every param change.

Q3. When use OnAfterRender? DOM ready; JS interop; chart init.

Q4. firstRender flag? true on first render only. Setup once.

Q5. StateHasChanged? Force re-render. Often unnecessary; Blazor auto re-renders after event/async.

Q6. InvokeAsync(StateHasChanged)? Marshal back to render context from timer/bg.

Q7. Cascading vs service state? Cascading: read-only ambient. Service: shared mutable; events.

Q8. PersistentComponentState? Server prerender → save state → WASM picks up. Avoid refetch.

Q9. ShouldRender? Skip render if no change. Perf.

Q10. Two-way binding? @bind="x". @bind:event="oninput" for keystroke updates.

Q11. Disposing JS references? Important — avoid leaks. IAsyncDisposable.

Q12. EventCallback vs delegate? EventCallback auto-StateHasChanged parent; Action doesn't.


Gotchas / common mistakes

  • ⚠️ JS in OnInitializedDOM not ready.
  • ⚠️ No Dispose for subscriptions.
  • ⚠️ Sync over async lifecycle — blocks.
  • ⚠️ Mutating Parameter properties — undefined behavior.
  • ⚠️ Forgetting StateHasChanged from background.

Further reading