Component Lifecycle & State
Key Points
- Lifecycle hooks:
SetParametersAsync→OnInitialized[Async]→OnParametersSet[Async]→ render →OnAfterRender[Async](firstRender).Disposeon tear-down. OnInitializedruns once per component instance;OnParametersSetruns whenever parent changes parameters.OnAfterRender(firstRender: true)is the only place to safely callIJSRuntime— DOM is ready.- State: cascading values (read-only ambient), state container (DI'd singleton),
PersistentComponentState(cross-render-mode). StateHasChangedqueues a re-render.ShouldRenderlets 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):
Parameters and EventCallbacks
@code {
[Parameter] public int Count { get; set; }
[Parameter] public EventCallback<int> CountChanged { get; set; }
}
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
Render happens with _data == null during the await. Show spinner; check null in razor.
Two-way binding
@bind:event="oninput" updates on every keystroke (default is onchange).
Component teardown
When parent removes component, Dispose runs. Clean subscribers / JS resources.
Capture references
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
✅ Correct: Dispose
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 OnInitialized — DOM not ready.
- ⚠️ No Dispose for subscriptions.
- ⚠️ Sync over async lifecycle — blocks.
- ⚠️ Mutating Parameter properties — undefined behavior.
- ⚠️ Forgetting StateHasChanged from background.