Blazor State Management Explained Simply
State management is one of the most important concepts in any Blazor application. Every interactive application has data that changes over time. A selected menu item, a counter value, the current user, a shopping cart, a form model, a filter, a theme preference or cached API data are all examples of state.
In small applications, state can feel simple. You keep a value in a component and update it when the user clicks a button. But as the application grows, state starts moving between components, pages, services and sometimes browser storage or the backend.
That is when state management becomes important.
The goal is not to make state management complicated. The goal is to keep data in the right place, with the right lifetime, and with a clear ownership model.
What is state in Blazor?
State is any data that affects what the user sees or how the application behaves.
For example:
- the selected tab in a UI
- the value of a search box
- the current authenticated user
- the selected device in a dashboard
- a loaded list of blog posts
- the current theme
- a form model
- notification messages
- temporary UI flags such as
IsLoading
In Blazor, state can live in several places. The most common options are:
- local component fields
- component parameters
- cascading values
- scoped or singleton services
- browser storage
- backend database or cache
Each option has a different purpose.
Good state management starts by choosing the simplest option that solves the problem.
1. Local component state
Local state is state that belongs only to one component.
This is the simplest form of state management in Blazor.
Example:
@code { private int counter = 0;private void Increment() { counter++; }}
<p>Counter: @counter</p>
<button class="btn btn-primary" @onclick="Increment"> Increment </button>
The counter variable belongs only to this component. No other component needs it. The value is used only for rendering this piece of UI.
This is a perfect use case for local state.
Use local state for:
- toggle flags
- temporary UI state
- selected tab inside one component
- loading indicators
- simple form state
- values that do not need to be shared
A common mistake is lifting state too early. If a value is only needed by one component, keep it there.
2. Parameters for parent-to-child state
When a parent component needs to pass data to a child component, use parameters.
Example:
<ProductCard Product="@product" />
Inside the child component:
[Parameter]
public ProductDto Product { get; set; } = default!;
Parameters are ideal when the parent owns the data and the child only needs to display it.
This keeps the data flow easy to understand.
Parent component:
@foreach (var product in Products)
{
<ProductCard Product="@product" />
}
Child component:
<h3>@Product.Name</h3>
<p>@Product.Description</p>
<strong>@Product.Price</strong>
Use parameters when:
- the parent owns the data
- the child only displays or uses the data
- the state naturally flows from top to bottom
This is one of the cleanest patterns in Blazor.
3. EventCallback for child-to-parent communication
Sometimes the child component needs to notify the parent that something happened.
For example, a user clicks a button inside a child component, but the parent should decide what to do.
Use EventCallback for that.
Child component:
[Parameter] public EventCallback<int> OnSelected { get; set; }
private async Task SelectItem(int id) { await OnSelected.InvokeAsync(id); }
Parent component:
<ProductCard Product="@product"
OnSelected="HandleProductSelected" />
Parent method:
private void HandleProductSelected(int productId)
{
selectedProductId = productId;
}
This is better than making the child component directly manipulate parent state or call unrelated services.
Use EventCallback when:
- a child component needs to report an action
- the parent should decide what happens next
- you want to keep components reusable
This keeps your component design clean and predictable.
4. Cascading values
Cascading values allow you to pass data down the component tree without manually passing it through every component.
Example:
<CascadingValue Value="Theme">
<MainLayout />
</CascadingValue>
Any child component can receive the value:
[CascadingParameter]
public ThemeSettings Theme { get; set; } = default!;
This is useful for state that many components need, but that should still flow from a higher-level owner.
Common examples:
- theme settings
- current culture
- layout configuration
- user context
- shared page-level state
Cascading values are powerful, but they should not be overused.
If everything becomes cascading, it becomes harder to understand where data comes from. Use cascading values for state that is genuinely shared across a meaningful part of the component tree.
5. Services for shared state
When state needs to be shared across unrelated components, a service is often a better choice.
Example:
public class AppState { public string? SelectedDeviceId { get; private set; }public event Action? OnChange; public void SetSelectedDevice(string deviceId) { SelectedDeviceId = deviceId; NotifyStateChanged(); } private void NotifyStateChanged() { OnChange?.Invoke(); }
}
Register the service:
builder.Services.AddScoped<AppState>();
Use it in a component:
@inject AppState AppState
<p>Selected device: @AppState.SelectedDeviceId</p>
Subscribe to changes:
protected override void OnInitialized() { AppState.OnChange += StateHasChanged; }
public void Dispose() { AppState.OnChange -= StateHasChanged; }
This pattern works well when several components need to react to the same state.
Use a shared state service for:
- selected device or selected entity
- dashboard filters
- notification messages
- user preferences during the session
- state shared between unrelated components
In Blazor Server, Scoped services are usually the safest default for user-specific state, because a scoped service belongs to the current circuit/user session.
6. Persistent state
Some state should survive a page refresh or browser restart.
For that, local component fields or scoped services are not enough. You need persistent storage.
Common options include:
- browser local storage
- browser session storage
- backend database
- distributed cache
- server-side user profile settings
Examples of persistent state:
- dark mode preference
- remembered filters
- selected language
- saved dashboard layout
- draft form values
- authentication-related session data
In Blazor WebAssembly, browser storage is often useful for client-side preferences.
In Blazor Server, you need to be more careful, because browser storage requires JavaScript interop and is only available after the component has rendered.
A common pattern is:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// load state from browser storage here
}
}
Use persistent state only when the data really needs to survive beyond the current component or session.
7. Backend state
Not all state belongs in the UI.
Some data should live in the backend, especially when it must be reliable, shared between users, auditable or secure.
Examples:
- user accounts
- orders
- payments
- blog posts
- permissions
- audit logs
- device readings
- system configuration
- long-term user preferences
A useful rule is this:
If losing the state would be a real problem, do not keep it only in the UI.
For example, a temporary selected tab can stay in the component. But a submitted order must be saved in the backend.
Choosing the right state management approach
The best state management approach depends on ownership and lifetime.
Ask these questions:
- Who owns the state?
- How long should it live?
- Which components need it?
- Should it survive refresh?
- Is it user-specific?
- Is it security-sensitive?
- Should it be stored permanently?
A simple decision guide:
- only one component needs it: use local state
- parent passes data to child: use parameters
- child notifies parent: use EventCallback
- many descendants need it: use cascading values
- unrelated components need it: use a scoped service
- it must survive refresh: use browser storage or backend
- it must be reliable and permanent: use a database
The simpler option is usually the better option.
Common mistakes
Using global state too early
Do not put everything in a shared service just because it is easy.
Global state can quickly make an application harder to reason about.
Keeping business data only in the UI
UI state is temporary. Important business data should be saved in the backend.
Overusing cascading values
Cascading values are useful, but they can hide dependencies if used too much.
Forgetting to unsubscribe from events
If you subscribe to state change events, unsubscribe when the component is disposed.
public void Dispose()
{
AppState.OnChange -= StateHasChanged;
}
This is especially important in Blazor Server applications.
Mixing UI logic and business logic
Components should not become the place where all business rules live.
Keep components focused on rendering and interaction. Move business logic into services or the application layer.
Best practices
Here are practical rules that work well in real applications:
- keep state as close as possible to where it is used
- prefer local state when possible
- use parameters for parent-to-child data flow
- use EventCallback for child-to-parent events
- use scoped services for shared user-specific state
- use persistent storage only when needed
- avoid unnecessary global state
- keep business logic out of components
- unsubscribe from events
- document shared state clearly
Good state management is mostly about discipline. Blazor gives you the tools, but the architecture is still your responsibility.
Final thoughts
Blazor state management does not need to be complicated.
Most applications can be built with a small number of simple patterns: local state, parameters, EventCallback, cascading values, shared services and persistent storage.
The key is to choose the right place for each piece of data.
If the state belongs to one component, keep it local. If it needs to move down the component tree, use parameters or cascading values. If unrelated components need to share it, use a service. If it needs to survive refresh or be reliable, store it in browser storage or the backend.
When state has a clear owner and a clear lifetime, your Blazor application becomes easier to understand, easier to debug and easier to maintain.
