A State Container is a service that preserves shared data and notifies interested components of its changes.
If you store a cart in a variable within ListaProductos.razor, the data disappears when the component is removed during navigation. To preserve it, we need an object with a longer lifespan than the page.
For data to survive navigation, we must extract it from components and move it to a safe place, a place that lives longer than the pages themselves. We call this State Management.
The simplest technique is to register a state container in DI.
What a State Container Contains
A State Container is a C# class that:
- Stores data (Properties).
- Has methods to modify that data.
- Fires an event when the data changes to notify components.
It is injected into components using the Dependency Injection (DI) system we saw in the previous block.
Creating the Container Class
Let’s create a classic example: a global counter that is visible on all pages.
public class CounterState
{
// 1. The data we want to store
public int Count { get; private set; }
// 2. The event to notify interested parties (Observer Pattern)
public event Action? OnChange;
// 3. Methods to modify the state
public void IncrementCount()
{
Count++;
NotifyStateChanged();
}
public void ResetCount()
{
Count = 0;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}Notice that the set accessor of Count is private. This is a good practice: it forces components to use the IncrementCount or ResetCount methods, preventing them from modifying the state “brute force.”
Registering the Service
Now we need to decide how long this state should live.
- If it should belong to the circuit or the client application, we use Scoped.
- If it should be shared among all server users, we could use Singleton, but only for truly global and concurrency-safe state.
The usual choice for user data (Cart, Preferences, Session Counter) is Scoped.
// Program.cs
builder.Services.AddScoped<CounterState>();In static SSR, Scoped means one instance per request, so this pattern does not preserve state between navigations. In Interactive Auto, you should also not assume that the server instance and the client instance share memory.
Consuming and Modifying the State
Any component can inject this service and call its methods.
Component A (The one that modifies):
@inject CounterState State
<button @onclick="State.IncrementCount">
Increment Global
</button>Reacting to Changes
The important detail lies in the service’s scope. If Component A modifies the value, Component B (which only displays the value) will not automatically know about it. Blazor does not monitor your service properties to repaint the screen.
The component that wants to display the updated value must subscribe to the OnChange event.
Component B (The one that displays):
@implements IDisposable
@inject CounterState State
<h3>Current count: @State.Count</h3>
@code {
protected override void OnInitialized()
{
// Subscribe: "When the state changes, execute StateHasChanged"
State.OnChange += OnStateChanged;
}
private void OnStateChanged()
{
_ = InvokeAsync(StateHasChanged);
}
public void Dispose()
{
State.OnChange -= OnStateChanged;
}
}Why StateHasChanged?
This method forces the component to re-render. By passing it as a delegate to the OnChange event, we are telling it: “Every time the service notifies a change, repaint me”.
Advantages of the Pattern
- Decoupling: Components do not talk to each other (
Parent -> Child), they talk to the Service (Component -> Service <- Component). - Persistence: You can navigate from
/hometo/profileand back to/home, and the counter will still hold the correct number, because theScopedservice was not destroyed. - Centralized Logic: The rules for modifying the data are in the
CounterStateclass, not scattered across the UI.
Limits of In-Memory State
In-memory state has a limit: The browser refresh (F5). Since the service lives in the RAM memory of the server (Blazor Server) or the browser (WASM), if the user presses F5, the memory is cleared and the counter returns to 0 (or the cart empties).
To solve this and make data survive even if the browser is closed, we need physical persistence. We don’t need a full database for this; the browser offers us LocalStorage.