Blazor State Management

This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14.

This section’s bibliography lists the reference material consulted while preparing these pages.

"State" means something different per render mode: a Static SSR component has none once the response is sent; an Interactive Server component’s state lives in a server-side circuit; an Interactive WebAssembly component’s state lives in the browser tab. Choosing a mechanism means being explicit about which of those applies.

State per render mode

Mode Where component state lives

Static SSR

Nowhere after the response is sent — each request starts from scratch.

Interactive Server

In the server’s memory, tied to the SignalR circuit; lost if the circuit is torn down and not reconnected (browser tab closed, extended network loss).

Interactive WebAssembly

In the browser tab’s memory; lost on reload/close, but the .NET runtime itself stays loaded across in-app navigations.

PersistentComponentState and [PersistentState]

The Blazor Web App SSR-to-interactive handover re-runs a component’s initialization once interactivity attaches (see the prerendering double-render trap discussed in the overview page) — PersistentComponentState carries data captured during the prerender pass across that handover so it isn’t fetched twice:

public sealed partial class WeatherPage : ComponentBase
{
    [Inject] public PersistentComponentState AppState { get; set; } = default!;

    private WeatherForecast[]? forecasts;
    private PersistingComponentStateSubscription subscription;

    protected override async Task OnInitializedAsync()
    {
        subscription = AppState.RegisterOnPersisting(() =>
        {
            AppState.PersistAsJson("forecasts", forecasts);
            return Task.CompletedTask;
        });

        if (!AppState.TryTakeFromJson<WeatherForecast[]>("forecasts", out var restored))
        {
            forecasts = await WeatherService.GetForecastsAsync();
        }
        else
        {
            forecasts = restored;
        }
    }

    public void Dispose() => subscription.Dispose();
}
NET 10 adds a declarative [PersistentState] attribute that removes the manual RegisterOnPersisting /

TryTakeFromJson boilerplate for the common case:

[PersistentState] public WeatherForecast[]? Forecasts { get; set; }

protected override async Task OnInitializedAsync()
{
    Forecasts ??= await WeatherService.GetForecastsAsync();
}

A custom PersistentComponentStateSerializer<T> controls how a non-trivially-serializable type is captured and restored, instead of relying on the default JSON serialization:

public sealed class ForecastSerializer : PersistentComponentStateSerializer<WeatherForecast[]>
{
    public override void Persist(WeatherForecast[] value, IBufferWriter<byte> writer) { /* custom encode */ }
    public override WeatherForecast[] Restore(ReadOnlySequence<byte> data) { /* custom decode */ throw new NotImplementedException(); }
}

Circuit state persistence across reconnects (.NET 10)

An Interactive Server circuit that loses its SignalR connection (a laptop sleeping, a brief network drop) used to lose all component state once the client’s reconnection window expired. .NET 10 can persist circuit state to a configured store so a reconnecting client resumes exactly where it left off, instead of a full reload:

builder.Services.AddRazorComponents()
    .AddInteractiveServerRenderMode()
    .AddCircuitOptions(o => o.PersistComponentStateOnReconnect = true);

This is separate from PersistentComponentState above — that one bridges the SSR-to-interactive handover; this one bridges a dropped-and-restored circuit.

In-memory state container services

For state shared across several components on one page (a shopping cart, a wizard’s accumulated form data), a DI-registered service is the idiomatic container. Its lifetime determines its scope:

public sealed class CartState
{
    public event Action? Changed;
    private readonly List<CartItem> items = [];
    public IReadOnlyList<CartItem> Items => items;

    public void Add(CartItem item) { items.Add(item); Changed?.Invoke(); }
}

// Interactive Server: Scoped == one instance per circuit (one browser tab)
// Interactive WebAssembly: Scoped == Singleton in practice, one runtime per tab
builder.Services.AddScoped<CartState>();

AddScoped gives one instance per circuit under Interactive Server, matching "per open tab" — but under Interactive WebAssembly there is exactly one DI container for the whole page load, so Scoped and Singleton behave identically there. AddSingleton under Interactive Server would instead share one instance across every connected user’s circuit, which is rarely what’s wanted for per-user state. See Dependency Injection for the general lifetime rules this builds on.

Browser storage

ProtectedLocalStorage / ProtectedSessionStorage (Interactive Server only, since they round-trip through the circuit) store values encrypted with Data Protection; from WebAssembly, call localStorage/sessionStorage directly through JS interop, or use the popular Blazored.LocalStorage package for a typed C# wrapper over the same browser APIs:

// dotnet add package Blazored.LocalStorage
builder.Services.AddBlazoredLocalStorage();
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage

@code {
    protected override async Task OnInitializedAsync()
        => draft = await LocalStorage.GetItemAsync<DraftInput>("draft");
}

State in the URL

Query parameters (see [SupplyParameterFromQuery]) keep filter/sort/page state shareable, bookmarkable, and survivable across a full reload — prefer this over any in-memory store for state a user would expect a refresh or a shared link to preserve.

Server-side stores

For state that must outlive a circuit/tab entirely (a saved cart across visits, a multi-day wizard), persist it through the app’s normal data layer — a database via EF Core, or a distributed cache — keyed by the authenticated user, not the circuit.

State-management libraries: Fluxor

Fluxor brings a Redux/Flux-style unidirectional store (actions, reducers, effects, a single immutable state tree) to Blazor, useful once ad hoc DI-scoped services and events become hard to reason about in a large app:

// dotnet add package Fluxor.Blazor.Web
builder.Services.AddFluxor(o => o.ScanAssemblies(typeof(Program).Assembly));

Reach for it when several unrelated components need to react to the same state changes with predictable, testable transitions — for a page-scoped cart or wizard, a plain scoped service is usually simpler.