
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
A 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 AppState, 80% 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:
- Static prerender
- Interactive render
If unmanaged, this causes:
- Duplicate API calls
- Lost UI state
- Confusing side effects
Using PersistentComponentState
- This code injects
PersistentComponentStateinto a component to persist afiltervalue. - 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
filterwhenever 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
PersistentComponentStatelifecycle - 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
OnParametersSetfor 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.