blazor-browser-storage-localstorage-sessionstorage

Persistencia en el navegador con localStorage y sessionStorage

  • 4 min

En el artículo anterior aprendimos a usar un State Container para compartir datos entre páginas. Sin embargo, su contenido vive en memoria: si recargamos la aplicación o termina el circuito, los datos se pierden.

Para conservar parte de ese estado, el navegador ofrece dos almacenes asociados al origen de la aplicación:

  1. localStorage: Conserva los datos entre sesiones hasta que el usuario, el navegador o la aplicación los elimine. Resulta útil para preferencias, borradores y carritos sin información sensible.
  2. sessionStorage: Conserva los datos durante la sesión de una pestaña y sobrevive a sus recargas. Resulta útil para formularios multipaso o filtros temporales.

El Browser Storage es una persistencia local, limitada y controlada por el usuario. No sustituye a una base de datos ni convierte en confiable la información guardada.

Desde C# accedemos a estas API mediante JS interop. Eso implica que no están disponibles durante el prerenderizado: debemos esperar a que el componente sea interactivo.

La opción que no conviene usar

En artículos antiguos encontrarás referencias a ProtectedBrowserStorage, que permitía guardar datos en localStorage o sessionStorage mediante ASP.NET Core Data Protection.

El matiz importante es que el paquete Microsoft.AspNetCore.ProtectedBrowserStorage fue experimental y no está pensado para producción. Además, en la documentación actual se mantiene como referencia histórica para aplicaciones antiguas, no como recomendación para una aplicación Blazor moderna.

No bases una aplicación nueva en ProtectedBrowserStorage.

Si necesitas proteger datos, guarda solo un identificador en el navegador y mantén el dato sensible en el servidor. El navegador siempre debe considerarse memoria controlada por el usuario.

Un servicio propio con JS interop

Podemos encapsular las llamadas a JavaScript y la serialización para que los componentes no repitan esta lógica:

using System.Text.Json;
using Microsoft.JSInterop;

public sealed class BrowserStorageService(IJSRuntime js)
{
    public async ValueTask SetLocalAsync<T>(string key, T value)
    {
        var json = JsonSerializer.Serialize(value);
        await js.InvokeVoidAsync("localStorage.setItem", key, json);
    }

    public async ValueTask<T?> GetLocalAsync<T>(string key)
    {
        var json = await js.InvokeAsync<string?>("localStorage.getItem", key);

        return json is null
            ? default
            : JsonSerializer.Deserialize<T>(json);
    }

    public ValueTask RemoveLocalAsync(string key) =>
        js.InvokeVoidAsync("localStorage.removeItem", key);
}
Copied!

Para trabajar con sessionStorage, podemos ofrecer métodos equivalentes que invoquen sessionStorage.setItem, sessionStorage.getItem y sessionStorage.removeItem.

Registramos nuestro servicio con el mismo ámbito que el estado de la interfaz:

builder.Services.AddScoped<BrowserStorageService>();
Copied!

Blazored.LocalStorage fue una opción popular, pero su repositorio se archivó en diciembre de 2025. En un proyecto existente podemos mantenerlo mientras evaluamos la migración; para código nuevo, una abstracción pequeña como esta evita depender de un paquete sin mantenimiento activo.

Cargar datos cuando el componente sea interactivo

Durante el prerenderizado no existe una conexión con las API del navegador. OnAfterRenderAsync se ejecuta cuando el componente ya puede usar JS interop:

@inject BrowserStorageService Storage

<button @onclick="GuardarCarrito">Guardar carrito</button>

@if (!cargado)
{
    <p>Cargando carrito...</p>
}

@code {
    private List<Producto> carrito = [];
    private bool cargado;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
        {
            return;
        }

        carrito = await Storage.GetLocalAsync<List<Producto>>("mi-carrito") ?? [];
        cargado = true;
        StateHasChanged();
    }

    private Task GuardarCarrito() =>
        Storage.SetLocalAsync("mi-carrito", carrito).AsTask();
}
Copied!

La llamada explícita a StateHasChanged es necesaria porque Blazor no vuelve a renderizar automáticamente cuando termina una tarea iniciada desde OnAfterRenderAsync.

Patrón de persistencia de estado

Conviene que los componentes no dependan directamente del mecanismo de persistencia. Podemos combinarlo con el State Container del artículo anterior:

  1. Cuando la interfaz ya es interactiva, el servicio de estado carga los datos del navegador.
  2. Al modificar el estado, actualiza la memoria y persiste el cambio.
public sealed class CarritoService(BrowserStorageService storage)
{
    public List<Producto> Items { get; private set; } = [];

    public async Task InitializeAsync()
    {
        Items = await storage.GetLocalAsync<List<Producto>>("cart") ?? [];
    }

    public async Task AddItemAsync(Producto producto)
    {
        Items.Add(producto);
        await storage.SetLocalAsync("cart", Items);
    }
}
Copied!

El componente puede llamar a InitializeAsync en su primer OnAfterRenderAsync, igual que en el ejemplo anterior.

Limitaciones y Seguridad:

  1. No guardes secretos: No almacenes contraseñas, tokens de acceso ni datos personales sensibles. Cualquier JavaScript ejecutado en el mismo origen puede leerlos.
  2. La cuota varía: El espacio disponible y las políticas de eliminación dependen del navegador. Guarda solo lo esencial y controla los errores de escritura.
  3. Los datos no son confiables: El usuario puede modificarlos. Valida siempre su contenido y contempla cambios de versión en el formato JSON.
  4. JS interop es asíncrono: Mantén estas operaciones fuera del renderizado y evita escribir en cada pulsación si puedes agrupar los cambios.