//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Usa el middleware persist para guardar y restaurar automáticamente el state del store en localStorage, sessionStorage o cualquier motor de almacenamiento personalizado.
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface PreferencesStore {
language: string;
currency: string;
setLanguage: (lang: string) => void;
setCurrency: (currency: string) => void;
}
export const usePreferencesStore = create<PreferencesStore>()(
persist(
(set) => ({
language: "en",
currency: "USD",
setLanguage: (language) => set({ language }),
setCurrency: (currency) => set({ currency }),
}),
{
name: "user-preferences", // clave de localStorage
}
)
);// stores/app-store.ts
import { create } from "zustand";
import { persist, createJSONStorage, StateStorage } from "zustand/middleware";
interface AppState {
user: { name: string; email: string } | null;
theme: "light" | "dark";
recentSearches: string[];
setUser: (user: AppState["user"]) => void;
setTheme: (theme: AppState["theme"]) => void;
addSearch: (query: string) => void;
clearSearches: () => void;
}
// Motor de almacenamiento personalizado con IndexedDB mediante idb-keyval
const indexedDBStorage: StateStorage = {
getItem: async (name) => {
const { get } = await import("idb-keyval");
return (await get(name)) || null;
},
setItem: async (name, value) => {
const { set } = await import("idb-keyval");
await set(name, value);
},
removeItem: async (name) => {
const { del } = await import("idb-keyval");
await del(name);
},
};
export const useAppStore = create<AppState>()(
persist(
(set) => ({
user: null,
theme: "light",
recentSearches: [],
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
addSearch: (query) =>
set((state) => ({
recentSearches: [
query,
...state.recentSearches.filter((s) => s !== query),
].slice(0, 10),
})),
clearSearches: () => set({ recentSearches: [] }),
}),
{
name: "app-storage",
storage: createJSONStorage(() => indexedDBStorage),
partialize: (state) => ({
theme: state.theme,
recentSearches: state.recentSearches,
// Excluir user - volver a obtener desde la API al cargar
}),
version: 2,
migrate: (persistedState: any, version) => {
if (version === 0) {
// v0 tenía "darkMode: boolean" en lugar de "theme"
persistedState.theme = persistedState.darkMode ? "dark" : "light";
delete persistedState.darkMode;
}
if (version < 2) {
// v1 tenía "searches" en lugar de "recentSearches"
persistedState.recentSearches = persistedState.searches || [];
delete persistedState.searches;
}
return persistedState;
},
}
)
);// components/hydration-gate.tsx
"use client";
import { useEffect, useState } from "react";
import { useAppStore } from "@/stores/app-store";
export function HydrationGate({ children }: { children: React.ReactNode }) {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
// Esperar a la rehidratación de persist
const unsub = useAppStore.persist.onFinishHydration(() => {
setHydrated(true);
});
// Si ya está hidratado
if (useAppStore.persist.hasHydrated()) {
setHydrated(true);
}
return unsub;
}, []);
if (!hydrated) return <div>Cargando...</div>;
return <>{children}</>;
}persist intercepta cada llamada a set y escribe el nuevo state en el almacenamiento después de la actualización del state.persist lee del almacenamiento y fusiona el state persistido con el state por defecto.partialize controla qué propiedades del state se guardan. Por defecto, se serializa todo el state (incluidas las funciones).version y migrate permiten la evolución del esquema. Cuando la versión almacenada es menor que la versión actual, migrate transforma el state antiguo.createJSONStorage envuelve una interfaz StateStorage con serialización y deserialización JSON.sessionStorage:
import { createJSONStorage } from "zustand/middleware";
persist(storeCreator, {
name: "session-store",
storage: createJSONStorage(() => sessionStorage),
});Persistencia selectiva con merge:
persist(storeCreator, {
name: "store",
partialize: (state) => ({ theme: state.theme, lang: state.lang }),
merge: (persistedState, currentState) => ({
...currentState,
...(persistedState as Partial<State>),
}),
});Almacenamiento cifrado:
const encryptedStorage: StateStorage = {
getItem: (name) => {
const raw = localStorage.getItem(name);
if (!raw) return null;
return decrypt(raw);
},
setItem: (name, value) => {
localStorage.setItem(name, encrypt(value));
},
removeItem: (name) => {
localStorage.removeItem(name);
},
};Borrar datos persistidos:
// Borrar el state persistido de forma programática
useAppStore.persist.clearStorage();
// O manualmente
localStorage.removeItem("app-storage");persist preserva la firma de tipos del store.partialize debe devolver un partial con tipos seguros. Usa Pick<State, keys> para un tipado explícito.migrate recibe unknown para el state persistido. Haz el cast con cuidado.persist<AppState>(storeCreator, {
name: "store",
partialize: (state): Pick<AppState, "theme" | "language"> => ({
theme: state.theme,
language: state.language,
}),
migrate: (persisted: unknown, version: number) => {
const state = persisted as Partial<AppState>;
return state as AppState;
},
});onFinishHydration o hasHydrated() para controlar el renderizado.null mediante JSON.stringify. Usa siempre partialize para excluir acciones, o se perderán y se reemplazarán por los valores por defecto en la rehidratación.version tiene por defecto 0. Si no defines una versión y luego necesitas migraciones, debes empezar desde la versión 0 en tu función migrate.storage para sincronización entre pestañas.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| localStorage (predeterminado) | Simple, lecturas sincrónicas | Límite de 5MB, bloquea el hilo principal |
| sessionStorage | Se borra automáticamente al cerrar la pestaña | No se comparte entre pestañas |
| IndexedDB | Gran capacidad, async | Configuración más compleja |
| Cookie storage | Disponible en el servidor (SSR) | Límite de 4KB, se envía con cada solicitud |
import { persist } from "zustand/middleware";
create<MyState>()(
persist(storeCreator, { name: "storage-key" })
);name (la clave de localStorage) es obligatoria.onFinishHydration o hasHydrated() para controlar el renderizado hasta que la hidratación termine.partialize controla qué propiedades del state se guardan en el almacenamiento.version en la configuración de persist.migrate que transforme el state antiguo a la nueva forma.migrate se ejecuta cuando la versión almacenada es menor que la versión actual.StateStorage (getItem, setItem, removeItem).createJSONStorage(() => yourStorage).storage para sincronización entre pestañas.useAppStore.persist.clearStorage();
// O manualmente:
localStorage.removeItem("app-storage");useAppStore.persist.onFinishHydration() en un useEffect para establecer un flag hydrated.useAppStore.persist.hasHydrated() para verificaciones sincrónicas.persist<AppState>(storeCreator, {
name: "store",
partialize: (state): Pick<AppState, "theme" | "language"> => ({
theme: state.theme,
language: state.language,
}),
});migrate recibe unknown para el state persistido y un number para la versión.const state = persisted as Partial<AppState>.createJSONStorage(() => sessionStorage) como opción storage.Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥