Mastering the Blazor Component Lifecycle
Stop memory leaks and race conditions. Master Blazor Server's lifecycle events and build a robust BaseComponent.
Introduction
In Blazor Server, a component is a C# class instance living on the server. If you don’t dispose of it correctly, it leaks memory. If you update the UI from a background thread incorrectly, it crashes.
Why lifecycle management matters:
- Memory Leaks: Subscribing to events without unsubscribing keeps the component alive forever.
- Race Conditions:
OnInitializedAsyncruns twice in Prerendering mode. - Responsiveness: Blocking the render thread freezes the UI.
What We’ll Build
- Lifecycle Map: Understanding exactly when methods run.
- Base Component: A reusable
BlueRobinComponentBasethat handlesIDisposableand CancellationTokens automatically. - Safe Updates: A helper for thread-safe UI updates.
Architecture Overview
flowchart TB
subgraph Lifecycle["🟣 Component Lifecycle"]
Constructor["🏗️ Constructor\n(DI Injection)"] --> SetParams["📥 SetParametersAsync\n(Properties Set)"]
SetParams --> Init["🚀 OnInitialized{Async}\n(Once per instance)"]
Init --> ParamSet["🔄 OnParametersSet{Async}\n(On Re-render)"]
ParamSet --> Build["🎨 BuildRenderTree\n(HTML Gen)"]
Build --> After["✅ OnAfterRender{Async}\n(JS Interop Safe)"]
After --> Dispose["🗑️ Dispose\n(Cleanup)"]
end
classDef primary fill:#7c3aed,color:#fff
classDef secondary fill:#06b6d4,color:#fff
classDef db fill:#f43f5e,color:#fff
classDef warning fill:#fbbf24,color:#000
class Lifecycle primary
Section 1: The Base Component Pattern
Don’t implement IDisposable in every component. Create a base class.
// Infrastructure/BlueRobinComponentBase.cs
public abstract class BlueRobinComponentBase : ComponentBase, IAsyncDisposable
{
private readonly CancellationTokenSource _cts = new();
// Auto-inject common services
[Inject] protected ILogger Logger { get; set; } = default!;
[Inject] protected NavigationManager Nav { get; set; } = default!;
// Pass this to all async calls (HTTP, DB).
// It cancels automatically when the user leaves the page.
protected CancellationToken ComponentToken => _cts.Token;
// Safe way to update UI from background threads (e.g., NATS Consumers)
protected Task SafeStateHasChangedAsync()
{
return _cts.IsCancellationRequested
? Task.CompletedTask
: InvokeAsync(StateHasChanged);
}
public async ValueTask DisposeAsync()
{
await _cts.CancelAsync();
_cts.Dispose();
GC.SuppressFinalize(this);
}
}
Section 2: Safe Async Loading
Typical pattern for loading data without freezing the UI.
@inherits BlueRobinComponentBase
@if (_isLoading)
{
<LoadingSpinner />
}
else
{
<h1>@_data.Title</h1>
}
@code {
private bool _isLoading = true;
private Document _data;
protected override async Task OnInitializedAsync()
{
try
{
// If the user leaves, LoadAsync cancels instantly
_data = await DocumentService.LoadAsync(Id, ComponentToken);
}
catch (OperationCanceledException) { /* Ignore */ }
finally
{
_isLoading = false;
}
}
}
Section 3: The OnAfterRender Trap
Never call JS Interop in OnInitialized. The DOM doesn’t exist yet. Use OnAfterRender.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Only safe to call JS here
await JS.InvokeVoidAsync("bluerobin.initializeChart", "myChart");
}
}
Conclusion
By inheriting from BlueRobinComponentBase, you eliminate 90% of the boilerplate associated with cancellation tokens and disposal. You ensure that every component is a good citizen effectively managing its own memory and lifecycle.