Built-in Lifecycle
Overview
The built-in lifecycle feature provides Redux-style application lifecycle state management for Blazor applications. It tracks initialization phases in the store, enabling components to show loading states, prevent user interaction during startup, and measure initialization performance.
Use this page to look up the built-in lifecycle surface, its actions, and its state model.
Minimum Setup
1. Register the Feature
// Program.cs
IReservoirBuilder reservoir = builder.AddReservoir();
reservoir.AddReservoirBlazorBuiltIns(); // Registers navigation + lifecycle
Or register lifecycle only:
IReservoirBuilder reservoir = builder.AddReservoir();
reservoir.AddBuiltInLifecycle();
2. Dispatch Lifecycle Actions
In your root component (e.g., MainLayout.razor.cs):
public partial class MainLayout : StoreComponent
{
[Inject]
private TimeProvider TimeProvider { get; set; } = default!;
protected override void OnInitialized()
{
base.OnInitialized();
Dispatch(new AppInitAction(TimeProvider.GetUtcNow()));
}
protected override async Task OnInitializedAsync()
{
// Replace with your initialization tasks (load preferences, establish connections, etc.)
await Task.CompletedTask;
Dispatch(new AppReadyAction(TimeProvider.GetUtcNow()));
}
}
3. React to Lifecycle State
public class LoadingOverlay : StoreComponent
{
private LifecycleState Lifecycle => GetState<LifecycleState>();
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (Lifecycle.Phase != LifecyclePhase.Ready)
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "loading-overlay");
builder.AddContent(2, "Loading...");
builder.CloseElement();
}
}
}
How It Works
- NotStarted - Initial state when the store is created
- Initializing - Entered when
AppInitActionis dispatched - Ready - Entered when
AppReadyActionis dispatched
Lifecycle Actions
AppInitAction
Dispatched when the application begins initialization.
// In your root component's OnInitialized
Dispatch(new AppInitAction(TimeProvider.GetUtcNow()));
| Parameter | Type | Description |
|---|---|---|
InitializedAt | DateTimeOffset | Timestamp when initialization began |
The reducer sets:
Phase→LifecyclePhase.InitializingInitializedAt→ the provided timestamp
AppReadyAction
Dispatched when the application has completed initialization.
// After all initialization tasks complete
// Replace with your initialization tasks (load preferences, establish connections, etc.)
await Task.CompletedTask;
Dispatch(new AppReadyAction(TimeProvider.GetUtcNow()));
| Parameter | Type | Description |
|---|---|---|
ReadyAt | DateTimeOffset | Timestamp when the app became ready |
The reducer sets:
Phase→LifecyclePhase.ReadyReadyAt→ the provided timestamp
LifecycleState
The LifecycleState feature state tracks lifecycle information:
public sealed record LifecycleState : IFeatureState
{
public static string FeatureKey => "reservoir:lifecycle";
public LifecyclePhase Phase { get; init; } = LifecyclePhase.NotStarted;
public DateTimeOffset? InitializedAt { get; init; }
public DateTimeOffset? ReadyAt { get; init; }
}
| Property | Description |
|---|---|
Phase | Current lifecycle phase (NotStarted, Initializing, Ready) |
InitializedAt | Timestamp when AppInitAction was dispatched |
ReadyAt | Timestamp when AppReadyAction was dispatched |
LifecyclePhase Enum
public enum LifecyclePhase
{
NotStarted = 0, // App has not started initialization
Initializing = 1, // App is currently initializing
Ready = 2 // App is ready for user interaction
}
Common Patterns
Loading Overlay
Show a loading overlay until the app is ready:
@inherits StoreComponent
@if (Lifecycle.Phase != LifecyclePhase.Ready)
{
<div class="loading-overlay">
<div class="spinner"></div>
<p>Loading application...</p>
</div>
}
@code {
private LifecycleState Lifecycle => GetState<LifecycleState>();
}
Disabled Interactions
Prevent button clicks during initialization:
@inherits StoreComponent
<button disabled="@(!IsReady)" @onclick="HandleClick">
Submit
</button>
@code {
private bool IsReady => GetState<LifecycleState>().Phase == LifecyclePhase.Ready;
private void HandleClick()
{
// Only reachable when ready
}
}
Initialization Performance Metrics
Calculate startup time:
public class PerformanceMonitor : StoreComponent
{
private LifecycleState Lifecycle => GetState<LifecycleState>();
private TimeSpan? StartupDuration =>
Lifecycle is { InitializedAt: { } init, ReadyAt: { } ready }
? ready - init
: null;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender && StartupDuration.HasValue)
{
Logger.LogInformation(
"App startup completed in {Duration}ms",
StartupDuration.Value.TotalMilliseconds);
}
}
}
Conditional Feature Loading
Load features only after the app is ready:
public class AnalyticsEffect : IActionEffect<LifecycleState>
{
public bool CanHandle(IAction action) => action is AppReadyAction;
public async IAsyncEnumerable<IAction> HandleAsync(
IAction action,
LifecycleState currentState,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
// Start analytics tracking after app is ready
await InitializeAnalyticsAsync();
yield break;
}
}
Why Caller-Supplied Timestamps?
The lifecycle actions require the caller to provide timestamps rather than generating them internally. This design choice ensures:
- Pure Reducers - Reducers remain pure functions (no side effects like reading system time)
- Testability - Tests can provide deterministic timestamps via
FakeTimeProvider - Consistency - Timestamps align with your app's time source (useful for distributed systems)
// Production code (inject TimeProvider)
[Inject]
private TimeProvider TimeProvider { get; set; } = default!;
Dispatch(new AppInitAction(TimeProvider.GetUtcNow()));
// Test code with deterministic time
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero));
Dispatch(new AppInitAction(fakeTime.GetUtcNow()));
Testing Lifecycle
Use the StoreTestHarness for unit testing lifecycle reducers:
[Fact]
public void AppInitAction_TransitionsToInitializing()
{
// Arrange
var timestamp = new DateTimeOffset(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
var harness = StoreTestHarnessFactory.ForFeature<LifecycleState>()
.WithReducer<AppInitAction>(LifecycleReducers.OnAppInit);
// Act & Assert
harness.CreateScenario()
.Given(new LifecycleState())
.When(new AppInitAction(timestamp))
.ThenState(state =>
{
state.Phase.Should().Be(LifecyclePhase.Initializing);
state.InitializedAt.Should().Be(timestamp);
});
}
[Fact]
public void AppReadyAction_TransitionsToReady()
{
// Arrange
var initTime = new DateTimeOffset(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
var readyTime = new DateTimeOffset(2024, 1, 15, 10, 30, 2, TimeSpan.Zero);
var harness = StoreTestHarnessFactory.ForFeature<LifecycleState>()
.WithReducer<AppInitAction>(LifecycleReducers.OnAppInit)
.WithReducer<AppReadyAction>(LifecycleReducers.OnAppReady);
// Act & Assert
harness.CreateScenario()
.Given(new LifecycleState())
.When(new AppInitAction(initTime))
.When(new AppReadyAction(readyTime))
.ThenState(state =>
{
state.Phase.Should().Be(LifecyclePhase.Ready);
state.InitializedAt.Should().Be(initTime);
state.ReadyAt.Should().Be(readyTime);
});
}
Summary
- built-in lifecycle tracks startup progress as explicit Reservoir state instead of implicit component state
- caller-supplied timestamps keep reducers pure and testable
- the feature is most useful when components and effects need a shared view of application readiness
Next Steps
- Reservoir Overview - Return to the full state-management model.
- Actions - Review the action model used by lifecycle updates.
- Reducers - Review the reducer pattern behind lifecycle transitions.
- Built-in Navigation - Pair readiness state with navigation state when needed.