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
OnInitializedis for synchronous setup;OnInitializedAsyncis for HTTP calls and async data loading.OnParametersSetfires 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.