State Management in Blazor
Manage shared state with service singletons, Fluxor, or cascading AppState to keep UI consistent across components.
State Management in Blazor is a free C# Academy lesson on CoddyKit. This is lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, and your progress syncs across the web and the CoddyKit app. The C# Academy course includes 4 lessons in total.
State Management Challenges in Blazor
As Blazor apps grow, managing state that must be shared across many unrelated components becomes complex. Simple prop-drilling breaks down; you need a centralized, reactive state store.
Simple State with a Scoped Service
The simplest Blazor state management: a scoped service that holds state and exposes an event so components can subscribe to changes.
public class CartState
{
private readonly List<CartItem> _items = new();
public IReadOnlyList<CartItem> Items => _items;
public int Count => _items.Sum(i => i.Quantity);
public event Action? OnChanged;
public void Add(Product p)
{
var existing = _items.FirstOrDefault(i => i.ProductId == p.Id);
if (existing is not null) existing.Quantity++;
else _items.Add(new CartItem { ProductId = p.Id, Name = p.Name, Price = p.Price });
OnChanged?.Invoke();
}
}Subscribing to State Changes
Components subscribe to the state service's OnChanged event and call StateHasChanged to re-render. Remember to unsubscribe in Dispose.
@inject CartState Cart
@implements IDisposable
<p>Cart: @Cart.Count items</p>
@code {
protected override void OnInitialized()
{
Cart.OnChanged += OnCartChanged;
}
private void OnCartChanged()
{
InvokeAsync(StateHasChanged);
}
public void Dispose() =>
Cart.OnChanged -= OnCartChanged;
}CascadingValue as a State Container
Wrap your root layout in a CascadingValue with your state service. All descendants can access it without explicit prop threading.
@* MainLayout.razor *@
@inject CartState Cart
<CascadingValue Value="Cart">
@Body
</CascadingValue>
@* Any nested component: *@
@code {
[CascadingParameter]
public CartState Cart { get; set; } = default!;
private void Buy(Product p) => Cart.Add(p);
}Fluxor: Flux Pattern for Blazor
Fluxor brings Redux-style unidirectional data flow to Blazor: State → Component → Action → Reducer → State. Great for complex applications.
// dotnet add package Fluxor.Blazor.Web
// State record
[FeatureState]
public record CounterState(int Count = 0);
// Actions
public record IncrementAction(int Amount = 1);
// Reducers
public static class CounterReducers
{
[ReducerMethod]
public static CounterState Reduce(CounterState state, IncrementAction action)
=> state with { Count = state.Count + action.Amount };
}
// Registration:
builder.Services.AddFluxor(opt =>
opt.ScanAssemblies(typeof(Program).Assembly));Using Fluxor in a Component
Inject IState<T> to read state and IDispatcher to dispatch actions. InheritsFrom<FluxorComponent> (or the mixin) auto-subscribes to state changes.
@inherits FluxorComponent
@inject IState<CounterState> CounterState
@inject IDispatcher Dispatcher
<p>Count: @CounterState.Value.Count</p>
<button @onclick="Increment">+1</button>
<button @onclick="() => Dispatcher.Dispatch(new IncrementAction(5))">+5</button>
@code {
private void Increment()
=> Dispatcher.Dispatch(new IncrementAction(1));
}LocalStorage Persistence
Persist state across page refreshes using browser localStorage via JavaScript interop. Restore it on app startup.
@inject IJSRuntime JS
@code {
private async Task SaveStateAsync()
{
var json = JsonSerializer.Serialize(_state);
await JS.InvokeVoidAsync("localStorage.setItem", "appState", json);
}
protected override async Task OnInitializedAsync()
{
var json = await JS.InvokeAsync<string?>("localStorage.getItem", "appState");
if (json is not null)
_state = JsonSerializer.Deserialize<AppState>(json) ?? new();
}
private AppState _state = new();
}Blazored.LocalStorage Package
The Blazored.LocalStorage package provides a clean, typed API over browser localStorage without hand-writing JS interop calls.
// dotnet add package Blazored.LocalStorage
builder.Services.AddBlazoredLocalStorage();
@inject ILocalStorageService LocalStorage
@code {
protected override async Task OnInitializedAsync()
{
_cart = await LocalStorage.GetItemAsync<List<CartItem>>("cart") ?? new();
}
private async Task SaveCartAsync()
{
await LocalStorage.SetItemAsync("cart", _cart);
}
private List<CartItem> _cart = new();
}URL State with NavigationManager
Use query parameters to hold state that should survive a page refresh and be bookmarkable. Read and write them via NavigationManager and the URI builder.
@inject NavigationManager Nav
@page "/products"
@code {
[SupplyParameterFromQuery] public string? Search { get; set; }
[SupplyParameterFromQuery] public int Page { get; set; } = 1;
private void UpdateSearch(string s)
{
Nav.NavigateTo(
Nav.GetUriWithQueryParameters(
new Dictionary<string, object?>
{
[nameof(Search)] = s,
[nameof(Page)] = 1
}));
}
}Real-World: Shopping Cart State
A complete pattern: scoped CartState service with events, persisted to localStorage on change, and restored on load.
public class CartState
{
private readonly ILocalStorageService _storage;
private List<CartItem> _items = new();
public event Action? OnChanged;
public CartState(ILocalStorageService storage) => _storage = storage;
public async Task InitAsync()
{
_items = await _storage.GetItemAsync<List<CartItem>>("cart") ?? new();
}
public async Task AddAsync(Product p)
{
var item = _items.FirstOrDefault(i => i.ProductId == p.Id);
if (item is not null) item.Quantity++;
else _items.Add(new CartItem { ProductId = p.Id, Name = p.Name });
await _storage.SetItemAsync("cart", _items);
OnChanged?.Invoke();
}
}Quick Check
Which state management approach is best for a simple Blazor app with limited shared state?
Recap: State Management in Blazor
Key takeaways:
- Scoped service + Action event: simplest shared state, good for most apps
- CascadingValue wrapping the root provides app-wide access without prop-drilling
- Fluxor: Redux-style unidirectional flow for complex state logic
- Persist state to localStorage with IJSRuntime or Blazored.LocalStorage
- URL state (SupplyParameterFromQuery) for bookmarkable, shareable UI state
- Always unsubscribe from events in Dispose to prevent memory leaks
Learn C# with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 93
- Lessons
- 346
Frequently Asked Questions
Is the “State Management in Blazor” lesson free?
Yes — the full text of “State Management in Blazor” is free to read here on the web. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO. The C# Academy course includes 4 lessons in total.
What will I learn in “State Management in Blazor”?
Manage shared state with service singletons, Fluxor, or cascading AppState to keep UI consistent across components. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 4 of 4.
How long does the “State Management in Blazor” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Blazor Component Model
- Data Binding & Event Handling
- Component Communication & DI
- State Management in Blazor