State Management in Blazor: Beyond Cascading Parameters

Read this article on Medium.

Cascading parameters are convenient.
They’re also one of the fastest ways to quietly sabotage a Blazor application.

Most Blazor apps don’t fail dramatically.
They fail subtly.

  • UI stops updating sometimes
  • State resets after navigation
  • Components refresh for no reason
  • Bugs disappear when you add StateHasChanged()

If any of that sounds familiar, the problem usually isn’t Blazor.
It’s state ownership.

Why State Management Is Tricky in Blazor

Blazor components are short-lived.
State usually is not.

That mismatch creates confusion early — especially because Blazor makes it easy to pass data around using dependency injection and cascading parameters, and the application works… until it grows.

The key realization is this:
Components render UI. State defines behavior.

When state has no clear owner, bugs emerge slowly and unpredictably.

Cascading Parameters: Useful, Until They Aren’t

Cascading parameters shine
when used for context, not behavior.

Good use cases:

  • Theme settings
  • Localization
  • Layout context
  • Read-only configuration

common pattern looks like this:

Enables global-style state access in Blazor by cascading an AppState object to all descendant components.

<CascadingValue Value="appState">
@Body
</CascadingValue>

[CascadingParameter]
public AppState State { get; set; } = default!;

Why This Breaks Down

  • Dependencies become invisible
  • Any child component can mutate shared state
  • Testing becomes difficult
  • Changes ripple through unrelated UI

Hard-Earned Lesson

A production application cascaded a mutable UserContext from the layout.
Months later, a deeply nested component updated it to toggle a flag.

  • No compiler errors.
  • No runtime exceptions.
  • Just incorrect UI across unrelated pages.

Cascading parameters hide dependencies
and hidden dependencies age badly.

The AppState Pattern: Simple, Explicit, Scalable

The AppState pattern introduces
clear ownership of UI state.

What It Solves

  • One place owns the state
  • Components subscribe intentionally
  • Updates are predictable
  • Easy to test and reason about

Example: AppState Container

A simple state container that centralizes UI state changes and notifies subscribers when state updates occur.

  • Encapsulates UI state
  • Emits change notifications
  • Prevents unnecessary re-renders
public class AppState
{
public event Action? OnChange;

private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set
{
if (_isBusy == value) return;
_isBusy = value;
NotifyStateChanged();
}
}

private void NotifyStateChanged() => OnChange?.Invoke();
}

Component Subscription

@inject AppState State

@code {
protected override void OnInitialized()
=> State.OnChange += StateHasChanged;

public void Dispose()
=> State.OnChange -= StateHasChanged;
}

Benefits:

  • Components react to state changes instead of owning state
  • No more scattered StateHasChanged() calls

Hard-Earned Lesson

A large Blazor Server application had manual refresh calls across dozens of components.

After introducing AppState80% of those calls disappeared — and so did several race-condition bugs.

If everyone owns state,
no one does.

Redux-Style Stores (Without the Redux Pain)

You don’t need Redux.
You need 
predictable state transitions.

Store Pattern Example

An immutable UI state store that updates state via explicit transitions and notifies subscribers when changes occur.

public record UiState(bool IsLoading, string? Error);

public class UiStore
{
public UiState State { get; private set; } = new(false, null);
public event Action? OnChange;

public void Dispatch(Func<UiState, UiState> update)
{
State = update(State);
OnChange?.Invoke();
}
}

Why This Works Well

  • Immutable updates prevent partial state corruption
  • State transitions are explicit
  • Easy to debug and test
  • Encourages disciplined updates

Example Usage

Dispatches an explicit, immutable state transition that sets IsLoading to true.

Store.Dispatch(state => state with { IsLoading = true });

Benefits:

  • You can’t accidentally forget to update related fields
  • Old state never leaks into new screens

Hard-Earned Lesson

A production UI showed stale error messages from previous pages.
The cause? Mutable state updated inconsistently.

Immutable state fixed the issue immediately.

Blazor State Lifetimes: The Part Everyone Gets Wrong

Scoped Services Are Not Durable

Blazor scoped services:
short-lived, often misunderstood, frequently misused.

The common mistake devs make: assuming scoped services survive longer than a user session.

What this means

  • Server disconnects reset scoped state
  • Refreshing the page resets WASM state
  • Server recycling wipes everything

Hard-Earned Lesson

An application worked perfectly until the server recycled.
Users lost in-progress UI state mid-session.

Scoped ≠ persistent.
Never assume it is.

Per-User vs Per-Session State (Critical Distinction)

One of the most common production bugs in Blazor SSR:

  • Storing identity or claims in AppState
  • Caching permissions in scoped services

Correct Mental Model

  • Authentication lives in the auth system
  • UI state lives in AppState
  • User data is fetched intentionally and cached carefully

Hard-Earned Lesson

A user logged out.
Another logged in.
Admin UI briefly appeared.

Cause?
Scoped state reused during circuit reconnect.

Authentication is not UI state.

Persisting State Across Pre-Render and Streaming (SSR)

Blazor SSR renders twice:

  1. Static prerender
  2. Interactive render

If unmanaged, this causes:

  • Duplicate API calls
  • Lost UI state
  • Confusing side effects

Using PersistentComponentState

  • This code injects PersistentComponentState into a component to persist a filter value.
  • On initialization, it attempts to restore the value from persisted JSON, defaulting to "all" if none exists.
  • It then registers a callback to save the current filter whenever the component state is being persisted, ensuring the UI state survives refreshes or reconnects.
[Inject]
PersistentComponentState Persistence { get; set; } = default!;

private const string Key = "filter";

protected override void OnInitialized()
{
if (!Persistence.TryTakeFromJson(Key, out string? filter))
{
filter = "all";
}

Persistence.RegisterOnPersisting(() =>
{
Persistence.PersistAsJson(Key, filter);
return Task.CompletedTask;
});
}

Benefit:

  • Prevents re-fetching during interactive handoff
  • Keeps SSR fast and predictable

What’s New in .NET 10 for Blazor State

.NET 10 continues refining Blazor SSR and state persistence.

.NET 10 makes Blazor state management more reliable, with clearer lifecycles, smoother SSR, and improved tooling.

Improvements You Get

  • More reliable PersistentComponentState lifecycle
  • Better streaming SSR coordination
  • Reduced duplication between prerender and interactive renders
  • Cleaner hooks for state hydration
  • Improved diagnostics around persistence

Practical Impact:

  • Fewer “double execution” bugs
  • Clearer separation between render phases
  • Safer persistence patterns without hacks

.NET 10 doesn’t change how you manage state — it makes doing it correctly easier and more reliable.

State Abuse: Hard Rules Earned the Hard Way

Experienced Blazor developers move from implicit, mutable state
to explicit, predictable, and feature-scoped state management.

Things experienced Blazor developers stop doing:

  • Cascading mutable state
  • Treating state like a database
  • Storing DTOs directly as UI state
  • Mutating state from arbitrary components
  • Calling services from property setters
  • Relying on OnParametersSet for side effects – can lead to unpredictable updates
  • Excessive component-level state — scattering state across too many components
  • Overusing cascading parameters — especially for frequently changing state
  • Direct DOM manipulation / JS interop for state — breaks declarative flow

Instead:

  • Explicit ownership
  • Immutable transitions
  • Feature-scoped stores
  • Predictable updates
  • Single source of truth per feature — keeps state predictable and testable
  • Subscription-based notifications — components react, don’t mutate
  • Immutable snapshots for UI — aligns with functional patterns and prevents accidental bugs

Final Thoughts: State Is Architecture

Blazor doesn’t need more patterns.
It needs 
clear ownership of state.

  • Components render UI.
  • Services execute logic.
  • State defines behavior.

Make state boring and your Blazor apps become more reliable.

Common State Management Mistakes in Blazor (And Why They Hurt)

Even experienced developers fall into these traps — not because they’re careless, but because Blazor makes them easy.

These are the mistakes I see most often in real production apps.

1. Treating Cascading Parameters as Global State

Cascading parameters were designed for context, not coordination.

Why it hurts

  • Hidden dependencies
  • Tight coupling
  • Hard-to-debug side effects
  • Unpredictable re-renders

Better approach
Use cascading parameters for layout context only. Move mutable state into an explicit AppState or store.

2. Letting Components Own Business State

When components start owning logic and state together, boundaries blur.

Why it hurts

  • Logic becomes UI-dependent
  • State resets unexpectedly
  • Testing becomes painful

Better approach

  • Components render.
  • Services and stores own behavior and state.

3. Overusing StateHasChanged() as a Fix

If you’re calling StateHasChanged() everywhere, the architecture is already telling you something is wrong.

Why it hurts

  • Masks real ownership issues
  • Causes excessive re-rendering
  • Leads to performance problems under load

Better approach

  • Let state containers notify subscribers.
  • Rendering should be a reaction, not a manual trigger.

4. Storing API DTOs Directly as UI State

DTOs are transport models — not UI models.

Why it hurts

  • UI becomes coupled to backend contracts
  • Partial updates corrupt state
  • Refactors become risky

Better approach

  • Map DTOs into UI-specific state objects or immutable records.

5. Mixing Authentication State with UI State

This one causes subtle and dangerous bugs in Blazor SSR.

Why it hurts

  • Identity leaks between users
  • Security issues during reconnects
  • Incorrect UI rendering

Better approach

  • Authentication lives in the auth system.
  • UI state reacts to it — but never stores it.

6. Assuming Scoped Services Are Persistent

Scoped feels safe — until it isn’t.

Why it hurts

  • Circuit disconnects reset state
  • Server restarts wipe everything
  • SSR handoffs behave differently than expected

Better approach

  • Treat scoped state as temporary.
  • Persist intentionally and sparingly.

7. Mutating Shared State from Anywhere

When any component can change anything, debugging becomes archaeology.

Why it hurts

  • Side effects appear far from the cause
  • State becomes unpredictable
  • Bugs emerge only under real usage

Better approach

  • Centralize mutations through methods or dispatch functions.
  • Make changes explicit.

State management isn’t about choosing the right pattern.
It’s about choosing 
clear ownership.

Blazor gives you powerful tools — cascading parameters, DI, scoped services — but those tools don’t replace architecture. They amplify it.

When state is:

  • Explicit
  • Predictable
  • Owned
  • Intentionally updated

Your application becomes:

  • Easier to reason about
  • Easier to test
  • Easier to scale
  • Easier to trust

Components render UI.
Services execute logic.
State defines behavior.

Make state boring and your Blazor applications will be anything but fragile.

Happy coding.