Un State Container es un servicio que conserva datos compartidos y notifica sus cambios a los componentes interesados.
Si guardas un carrito en una variable de ListaProductos.razor, el dato desaparece cuando el componente se retira al navegar. Para conservarlo, necesitamos un objeto con una vida mayor que la página.
Para que los datos sobrevivan a la navegación, debemos sacarlos de los componentes y moverlos a un lugar seguro, un lugar que viva más tiempo que las propias páginas. A esto lo llamamos Gestión de Estado (State Management).
La técnica más sencilla es registrar un contenedor de estado en DI.
Qué contiene un State Container
Un State Container es una clase C# que:
- Almacena datos (Propiedades).
- Tiene métodos para modificar esos datos.
- Dispara un evento cuando los datos cambian para avisar a los componentes.
Se inyecta en los componentes usando el sistema de Inyección de Dependencias (DI) que vimos en el bloque anterior.
Crear la clase contenedora
Vamos a crear un ejemplo clásico: un contador global que se ve en todas las páginas.
public class CounterState
{
// 1. El dato que queremos guardar
public int Count { get; private set; }
// 2. El evento para avisar a los interesados (Observer Pattern)
public event Action? OnChange;
// 3. Métodos para modificar el estado
public void IncrementCount()
{
Count++;
NotifyStateChanged();
}
public void ResetCount()
{
Count = 0;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}Fíjate que el set de Count es privado. Esto es una buena práctica: obligamos a los componentes a usar los métodos IncrementCount o ResetCount, impidiendo que modifiquen el estado “a lo bruto”.
Registrar el servicio
Ahora debemos decidir cuánto tiempo vive este estado.
- Si debe pertenecer al circuito o a la aplicación cliente, usamos Scoped.
- Si debe compartirse entre todos los usuarios del servidor, podríamos usar Singleton, pero solo para estado realmente global y seguro frente a concurrencia.
Lo habitual para datos de usuario (Carrito, Preferencias, Contador de sesión) es Scoped.
// Program.cs
builder.Services.AddScoped<CounterState>();En SSR estático, Scoped significa una instancia por petición, así que este patrón no conserva estado entre navegaciones. En Interactive Auto tampoco debes asumir que la instancia del servidor y la del cliente comparten memoria.
Consumir y modificar el estado
Cualquier componente puede inyectar este servicio y llamar a sus métodos.
Componente A (El que modifica):
@inject CounterState State
<button @onclick="State.IncrementCount">
Incrementar Global
</button>Reaccionar a los cambios
El detalle importante está en el ámbito del servicio. Si el Componente A modifica el valor, el Componente B (que solo muestra el valor) no se enterará automáticamente. Blazor no monitoriza las propiedades de tus servicios para repintar la pantalla.
El componente que quiera mostrar el valor actualizado debe suscribirse al evento OnChange.
Componente B (El que muestra):
@implements IDisposable
@inject CounterState State
<h3>Cuenta actual: @State.Count</h3>
@code {
protected override void OnInitialized()
{
// Nos suscribimos: "Cuando cambie el estado, ejecuta StateHasChanged"
State.OnChange += OnStateChanged;
}
private void OnStateChanged()
{
_ = InvokeAsync(StateHasChanged);
}
public void Dispose()
{
State.OnChange -= OnStateChanged;
}
}¿Por qué StateHasChanged?
Este método fuerza al componente a re-renderizarse. Al pasarlo como delegado al evento OnChange, le estamos diciendo: “Cada vez que el servicio avise de un cambio, repíntame”.
Ventajas del patrón
- Desacoplamiento: Los componentes no hablan entre sí (
Padre -> Hijo), hablan con el Servicio (Componente -> Servicio <- Componente). - Persistencia: Puedes navegar de
/homea/perfily volver a/home, y el contador seguirá en el número correcto, porque el servicioScopedno se destruyó. - Lógica Centralizada: Las reglas para modificar los datos están en la clase
CounterState, no dispersas por la UI.
Límites del estado en memoria
El estado en memoria tiene un límite: El refresco del navegador (F5). Como el servicio vive en la memoria RAM del servidor (Blazor Server) o del navegador (WASM), si el usuario pulsa F5, la memoria se limpia y el contador vuelve a 0 (o el carrito se vacía).
Para solucionar esto y hacer que los datos sobrevivan incluso si se cierra el navegador, necesitamos persistencia física. No necesitamos una base de datos completa para esto; el navegador nos ofrece LocalStorage.