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();
}
[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.