//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Zustand incluye varios middleware integrados: persist para almacenamiento, devtools para Redux DevTools, immer para actualizaciones inmutables y subscribeWithSelector para suscripciones granulares. Apílalos anidándolos.
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
interface AppState {
count: number;
increment: () => void;
}
export const useStore = create<AppState>()(
devtools(
persist(
immer((set) => ({
count: 0,
increment: () =>
set((state) => {
state.count += 1; // Mutación directa gracias a immer
}),
})),
{ name: "app-store" }
),
{ name: "AppStore" }
)
);// stores/settings-store.ts
import { create } from "zustand";
import { devtools, persist, subscribeWithSelector } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
interface Settings {
theme: "light" | "dark" | "system";
fontSize: number;
language: string;
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
}
interface SettingsStore extends Settings {
setTheme: (theme: Settings["theme"]) => void;
setFontSize: (size: number) => void;
setLanguage: (lang: string) => void;
toggleNotification: (channel: keyof Settings["notifications"]) => void;
resetToDefaults: () => void;
}
const defaults: Settings = {
theme: "system",
fontSize: 16,
language: "en",
notifications: { email: true, push: true, sms: false },
};
export const useSettingsStore = create<SettingsStore>()(
devtools(
subscribeWithSelector(
persist(
immer((set) => ({
...defaults,
setTheme: (theme) =>
set((state) => {
state.theme = theme;
}),
setFontSize: (size) =>
set((state) => {
state.fontSize = Math.max(12, Math.min(24, size));
}),
setLanguage: (lang) =>
set((state) => {
state.language = lang;
}),
toggleNotification: (channel) =>
set((state) => {
state.notifications[channel] = !state.notifications[channel];
}),
resetToDefaults: () =>
set((state) => {
Object.assign(state, defaults);
}),
})),
{
name: "settings-storage",
partialize: (state) => ({
theme: state.theme,
fontSize: state.fontSize,
language: state.language,
notifications: state.notifications,
}),
}
)
),
{ name: "SettingsStore" }
)
);
// Suscribirse a cambios específicos del state
useSettingsStore.subscribe(
(state) => state.theme,
(theme) => {
document.documentElement.setAttribute("data-theme", theme);
}
);
useSettingsStore.subscribe(
(state) => state.fontSize,
(fontSize) => {
document.documentElement.style.fontSize = `${fontSize}px`;
}
);// components/settings-panel.tsx
"use client";
import { useSettingsStore } from "@/stores/settings-store";
export function SettingsPanel() {
const theme = useSettingsStore((s) => s.theme);
const fontSize = useSettingsStore((s) => s.fontSize);
const notifications = useSettingsStore((s) => s.notifications);
const setTheme = useSettingsStore((s) => s.setTheme);
const setFontSize = useSettingsStore((s) => s.setFontSize);
const toggleNotification = useSettingsStore((s) => s.toggleNotification);
const resetToDefaults = useSettingsStore((s) => s.resetToDefaults);
return (
<div>
<section>
<h3>Tema</h3>
<select value={theme} onChange={(e) => setTheme(e.target.value as any)}>
<option value="light">Claro</option>
<option value="dark">Oscuro</option>
<option value="system">Sistema</option>
</select>
</section>
<section>
<h3>Tamaño de fuente: {fontSize}px</h3>
<input
type="range"
min={12}
max={24}
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
/>
</section>
<section>
<h3>Notificaciones</h3>
{(Object.keys(notifications) as Array<keyof typeof notifications>).map((ch) => (
<label key={ch}>
<input
type="checkbox"
checked={notifications[ch]}
onChange={() => toggleNotification(ch)}
/>
{ch}
</label>
))}
</section>
<button onClick={resetToDefaults}>Restablecer valores por defecto</button>
</div>
);
}set para que puedas escribir actualizaciones con estilo mutable que producen state inmutable..subscribe() para aceptar un selector y una función de igualdad, permitiendo efectos secundarios granulares.Orden del middleware (recomendado):
// devtools más externo, luego subscribeWithSelector, luego persist, luego immer más interno
create<Store>()(
devtools(
subscribeWithSelector(
persist(
immer((set) => ({ ... })),
{ name: "store" }
)
)
)
);Middleware personalizado:
import { StateCreator, StoreMutatorIdentifier } from "zustand";
type Logger = <
T,
Mps extends [StoreMutatorIdentifier, unknown][] = [],
Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
f: StateCreator<T, Mps, Mcs>,
name?: string
) => StateCreator<T, Mps, Mcs>;
const logger: Logger = (f, name) => (set, get, store) => {
const loggedSet: typeof set = (...args) => {
set(...(args as any));
console.log(`[${name || "store"}]`, get());
};
return f(loggedSet, get, store);
};
// Uso
const useStore = create(logger((set) => ({ count: 0 }), "CountStore"));create<State>()() (doble invocación) para ayudar a TypeScript a inferir los tipos combinados.partialize en el middleware persist debe devolver un subconjunto del state con tipos seguros.// El patrón ()() es necesario para la inferencia de tipos del middleware
export const useStore = create<State>()(
devtools(persist(immer((set) => ({ ... })), { name: "store" }))
);devtools como el más externo e immer como el más interno.immer sin importar el middleware desde zustand/middleware/immer no funcionará. Es un paquete separado.persist serializa el state con JSON.stringify por defecto. Las funciones, Dates, Maps y Sets se perderán. Usa partialize para excluir valores no serializables.devtools añade sobrecarga. Desactívalo en producción: devtools(fn, { enabled: process.env.NODE_ENV === "development" }).subscribeWithSelector debe envolver persist (no al revés) si quieres suscribirte a cambios del state persistido.| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Middleware integrados de Zustand | Soporte oficial, bien tipados | Conjunto fijo de middleware |
| Middleware personalizado | Adaptado a tus necesidades | Debes gestionar los tipos manualmente |
| Sin middleware (store simple) | Lo más simple y rápido | Sin persistencia, devtools ni immer |
| Middleware de Redux Toolkit | Ecosistema maduro | Biblioteca distinta, más boilerplate |
persist -- guarda y restaura el state en almacenamiento (localStorage por defecto).devtools -- se conecta a la extensión de navegador Redux DevTools.immer -- permite actualizaciones de state con estilo mutable que producen state inmutable.subscribeWithSelector -- permite suscribirse a fragmentos específicos del state fuera de React.devtools como el más externo, luego subscribeWithSelector, luego persist y immer como el más interno.() ayuda a TypeScript a inferir correctamente los tipos combinados del middleware..subscribe() para aceptar un selector y una función de igualdad.useStore.subscribe(
(state) => state.theme,
(theme) => document.documentElement.setAttribute("data-theme", theme)
);set, get o store.persist o devtools no funcionará correctamente.JSON.stringify serializa las funciones como null.partialize para excluir acciones y valores no serializables.devtools(storeCreator, {
name: "AppStore",
enabled: process.env.NODE_ENV === "development",
});subscribeWithSelector debe envolver persist (no al revés) si quieres suscribirte a cambios del state persistido.create<State>()() para que TypeScript infiera correctamente los tipos combinados.persist(storeCreator, {
name: "settings-storage",
partialize: (state) => ({
theme: state.theme,
fontSize: state.fontSize,
}),
});Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥