//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Estas recetas de skills están diseñadas para Claude Code, pero también funcionan con otros agentes de codificación con IA que admitan archivos de skill/instrucciones.
El contenido completo de SKILL.md que puedes copiar en .claude/skills/zustand-state-management/SKILL.md:
---
name: zustand-state-management
description: "Construcción de state global escalable y performante con Zustand y TypeScript. Úsalo cuando te pidan: ayuda con zustand, state global, patrón de store, gestión de state, selectores de zustand, middleware de zustand, persist de zustand, SSR con zustand."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Agent"
---
# Gestión de State con Zustand
Eres un experto en Zustand. Ayuda a los desarrolladores a construir gestión de state escalable, performante y bien tipada.
## Reglas de Arquitectura de Stores
1. **Un store por dominio** - store de auth, store de carrito, store de UI. Nunca un store gigante.
2. **State plano** - Evita objetos profundamente anidados. Normaliza los datos como en una base de datos.
3. **Coloca las acciones junto al state** - Mantén las acciones en el mismo store que el state que modifican.
4. **Usa selectores** - Nunca te suscribas al store completo. Selecciona siempre los datos mínimos necesarios.
5. **Deriva, no almacenes** - Los valores calculados deben derivarse en selectores, no almacenarse.
## Patrones Principales
### Store Básico con TypeScript
```tsx
import \{ create \} from "zustand";
interface CounterState \{
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
\}
const useCounterStore = create<CounterState>((set) => (\{
count: 0,
increment: () => set((state) => (\{ count: state.count + 1 \})),
decrement: () => set((state) => (\{ count: state.count - 1 \})),
reset: () => set(\{ count: 0 \}),
\}));// MAL - se suscribe al store completo, re-renderiza con CUALQUIER cambio
function Component() \{
const store = useCounterStore();
return <span>\{store.count\}</span>;
\}
// BIEN - se suscribe solo a count
function Component() \{
const count = useCounterStore((state) => state.count);
return <span>\{count\}</span>;
\}
// BIEN - múltiples valores con comparación superficial
import \{ useShallow \} from "zustand/react/shallow";
function Component() \{
const \{ count, increment \} = useCounterStore(
useShallow((state) => (\{ count: state.count, increment: state.increment \}))
);
return <button onClick=\{increment\}>\{count\}</button>;
\}
// BIEN - referencia estable de acción (las acciones nunca cambian)
function Component() \{
const increment = useCounterStore((state) => state.increment);
// la referencia de increment es estable, sin re-renderizados por otros cambios de state
\}interface AuthSlice \{
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
\}
interface CartSlice \{
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: () => number;
\}
const createAuthSlice: StateCreator<AuthSlice & CartSlice, [], [], AuthSlice> = (
set
) => (\{
user: null,
login: async (credentials) => \{
const user = await api.login(credentials);
set(\{ user \});
\},
logout: () => set(\{ user: null \}),
\});
const createCartSlice: StateCreator<AuthSlice & CartSlice, [], [], CartSlice> = (
set,
get
) => (\{
items: [],
addItem: (item) => set((state) => (\{ items: [...state.items, item] \})),
removeItem: (id) =>
set((state) => (\{ items: state.items.filter((i) => i.id !== id) \})),
total: () => get().items.reduce((sum, item) => sum + item.price, 0),
\});
const useStore = create<AuthSlice & CartSlice>()((...args) => (\{
...createAuthSlice(...args),
...createCartSlice(...args),
\}));import \{ persist, createJSONStorage \} from "zustand/middleware";
const useSettingsStore = create<SettingsState>()(
persist(
(set) => (\{
theme: "light" as const,
language: "en",
setTheme: (theme: "light" | "dark") => set(\{ theme \}),
setLanguage: (language: string) => set(\{ language \}),
\}),
\{
name: "settings-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => (\{
theme: state.theme,
language: state.language,
\}), // Solo persistir estos campos, no las acciones
\}
)
);import \{ devtools \} from "zustand/middleware";
const useStore = create<StoreState>()(
devtools(
(set) => (\{
count: 0,
increment: () =>
set(
(state) => (\{ count: state.count + 1 \}),
false,
"increment" // nombre de la acción en devtools
),
\}),
\{ name: "MyStore" \}
)
);import \{ immer \} from "zustand/middleware/immer";
const useTodoStore = create<TodoState>()(
immer((set) => (\{
todos: [],
toggleTodo: (id: string) =>
set((state) => \{
const todo = state.todos.find((t) => t.id === id);
if (todo) todo.completed = !todo.completed; // la mutación directa es segura con immer
\}),
addTodo: (text: string) =>
set((state) => \{
state.todos.push(\{ id: crypto.randomUUID(), text, completed: false \});
\}),
\}))
);const useStore = create<StoreState>()(
devtools(
persist(
immer((set) => (\{
// definición del store
\})),
\{ name: "my-store" \}
),
\{ name: "MyStore" \}
)
);
// Orden: immer (más interno) -> persist -> devtools (más externo)// stores/counter-store.ts
import \{ create \} from "zustand";
interface CounterState \{
count: number;
increment: () => void;
\}
export const useCounterStore = create<CounterState>((set) => (\{
count: 0,
increment: () => set((state) => (\{ count: state.count + 1 \})),
\}));
// Para evitar desincronización de hidratación con el middleware persist:
// components/HydrationGuard.tsx
"use client";
import \{ useEffect, useState \} from "react";
export function HydrationGuard(\{ children \}: \{ children: React.ReactNode \}) \{
const [hydrated, setHydrated] = useState(false);
useEffect(() => setHydrated(true), []);
return hydrated ? <>\{children\}</> : null; // o un skeleton
\}interface ProductState \{
products: Product[];
loading: boolean;
error: string | null;
fetchProducts: () => Promise<void>;
\}
const useProductStore = create<ProductState>((set) => (\{
products: [],
loading: false,
error: null,
fetchProducts: async () => \{
set(\{ loading: true, error: null \});
try \{
const products = await api.getProducts();
set(\{ products, loading: false \});
\} catch (error) \{
set(\{ error: (error as Error).message, loading: false \});
\}
\},
\}));import \{ renderHook, act \} from "@testing-library/react";
import \{ useCounterStore \} from "./counter-store";
// Restablecer el store entre pruebas
beforeEach(() => \{
useCounterStore.setState(\{ count: 0 \});
\});
test("increment aumenta count", () => \{
const \{ result \} = renderHook(() =>
useCounterStore((state) => (\{
count: state.count,
increment: state.increment,
\}))
);
act(() => result.current.increment());
expect(result.current.count).toBe(1);
\});
// Probar el store directamente sin React
test("el store funciona sin componentes", () => \{
const \{ getState \} = useCounterStore;
expect(getState().count).toBe(0);
getState().increment();
expect(getState().count).toBe(1);
\});
## Ejemplo funcional
### Ejemplo 1: El usuario pide "Configura un store de Zustand para mi carrito de compras"
**Prompt del usuario:** "Necesito un store de carrito con añadir, eliminar, actualizar cantidad y cálculo del total."
**La respuesta guiada por el skill produciría:**
```tsx
// stores/cart-store.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
addItem: (item: Omit<CartItem, "quantity">) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
}
export const useCartStore = create<CartState>()(
persist(
immer((set) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
existing.quantity += 1;
} else {
state.items.push({ ...item, quantity: 1 });
}
}),
removeItem: (id) =>
set((state) => {
state.items = state.items.filter((i) => i.id !== id);
}),
updateQuantity: (id, quantity) =>
set((state) => {
const item = state.items.find((i) => i.id === id);
if (item) item.quantity = Math.max(0, quantity);
}),
clearCart: () => set({ items: [] }),
})),
{
name: "cart-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ items: state.items }),
}
)
);
// Selectores derivados (definidos fuera del store)
export const selectCartTotal = (state: CartState) =>
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
export const selectCartCount = (state: CartState) =>
state.items.reduce((sum, item) => sum + item.quantity, 0);
Respuesta guiada por el skill: Diagnosticar el patrón de selector: probablemente se está suscribiendo al store completo o se usa un selector de objeto sin useShallow.
Este skill proporciona:
mkdir -p .claude/skills/zustand-state-management
# Pega el contenido de la Receta en .claude/skills/zustand-state-management/SKILL.mdset({ count: 1 }) fusiona con el state existente, no lo reemplaza. Usa set(state => state, true) para un reemplazo completo.onRehydrateStorage o un guard de hidratación.| Enfoque | Cuándo usarlo |
|---|---|
| Jotai | Modelo de state atómico, enfoque bottom-up |
| Valtio | Basado en proxy, API mutable |
| Redux Toolkit | Equipos grandes, necesidades complejas de middleware, depuración con time-travel |
| React Context | State simple compartido entre pocos componentes |
| Signals (Preact) | Reactividad granular sin selectores |
// Mal: re-renderiza con CUALQUIER cambio de state
const store = useCounterStore();
// Bien: se suscribe solo a count
const count = useCounterStore((state) => state.count);import { useShallow } from "zustand/react/shallow";
const { count, increment } = useCounterStore(
useShallow((state) => ({ count: state.count, increment: state.increment }))
);useShallow cuando tu selector devuelve un objeto (múltiples valores)from "zustand/react/shallow"zustandimport { create } from "zustand";
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
}
const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));create<State>()const useStore = create<StoreState>()(
devtools(
persist(
immer((set) => ({ /* ... */ })),
{ name: "my-store" }
),
{ name: "MyStore" }
)
);persist guarda el state del store en localStorage (u otro almacenamiento)partialize te permite seleccionar qué campos persistir (excluye acciones y datos derivados)createJSONStorage(() => localStorage) como adaptador de almacenamientoset({ count: 1 }) fusiona de forma superficial con el state existente (no reemplaza el store completo)set(newState, true) con la bandera replaceHydrationGuard que espere a useEffect antes de renderizaronRehydrateStorage del middleware persistStateCreatorcreate()// Restablecer el store entre pruebas
beforeEach(() => {
useCounterStore.setState({ count: 0 });
});
// Probar sin React
test("increment funciona", () => {
const { getState } = useCounterStore;
getState().increment();
expect(getState().count).toBe(1);
});setState para restablecer entre pruebasRevisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥