Skip to main content

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

  1. NotStarted - Initial state when the store is created
  2. Initializing - Entered when AppInitAction is dispatched
  3. Ready - Entered when AppReadyAction is dispatched

Lifecycle Actions

AppInitAction

Dispatched when the application begins initialization.

// In your root component's OnInitialized
Dispatch(new AppInitAction(TimeProvider.GetUtcNow()));
ParameterTypeDescription
InitializedAtDateTimeOffsetTimestamp when initialization began

The reducer sets:

  • PhaseLifecyclePhase.Initializing
  • InitializedAt → 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()));
ParameterTypeDescription
ReadyAtDateTimeOffsetTimestamp when the app became ready

The reducer sets:

  • PhaseLifecyclePhase.Ready
  • ReadyAt → 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; }
}
PropertyDescription
PhaseCurrent lifecycle phase (NotStarted, Initializing, Ready)
InitializedAtTimestamp when AppInitAction was dispatched
ReadyAtTimestamp 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:

  1. Pure Reducers - Reducers remain pure functions (no side effects like reading system time)
  2. Testability - Tests can provide deterministic timestamps via FakeTimeProvider
  3. 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.