Communitygithub.com

use-zustand

Guía autorizada para usar Zustand en proyectos React junto con TanStack Query. Define reglas para gestionar estado de cliente, evitar duplicación con datos del servidor y escribir stores tipados, modulares y testables.

¿Qué es use-zustand?

Esta skill proporciona un conjunto completo de reglas y patrones para usar Zustand de manera efectiva en proyectos React, priorizando la separación de responsabilidades con TanStack Query. El estado del lado del cliente (UI, formularios paso a paso, carrito en edición, tema) se gestiona con Zustand, mientras que los datos del servidor se delegan a TanStack Query para evitar fuentes de verdad duplicadas. La skill enseña a organizar stores grandes mediante el patrón de slices en lugar de un único objeto plano, a usar acciones con nombre en vez de llamadas directas a `set()`, y a seleccionar con precisión la porción de estado necesaria para evitar rerenders innecesarios. También cubre middleware como `immer` para actualizaciones anidadas, `persist` para estado persistente, `devtools` para depuración con nombre de acciones, y la creación de stores tipados con la forma currificada `create<State>()(...)`. Finalmente, promueve la testabilidad exportando el creador de store subyacente junto con el hook, y recomienda el uso de `useShallow` para selecciones de objetos o múltiples valores.

Compatible con~Claude Code~Codex CLI~Cursor
npx skills add C1131-G/skills

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

¿Qué hace use-zustand?

Usa esta skill directamente para el estado de Zustand. Combínala con use-tanstack-query para mantener el estado del cliente y el estado del servidor en sus respectivos dueños.

También aplica enforce-code-quality y enforce-typescript-strict a los archivos involucrados.

Aplica estas reglas cada vez que escribas o revises stores de Zustand en un proyecto React.

1. Zustand es solo para estado del cliente — no se superpone con TanStack Query

Zustand gestiona el estado de UI/cliente: sidebar abierto/cerrado, datos de formularios de varios pasos, tema, contenido del carrito que el usuario está editando activamente. Los datos del servidor (cualquier cosa obtenida de una API) pertenecen a TanStack Query, no se duplican en un store de Zustand — consulta use-tanstack-query. Mezclar ambas responsabilidades (por ejemplo, copiar datos obtenidos del servidor en Zustand "para facilitar el acceso") crea dos fuentes de verdad que pueden desincronizarse.

2. No se necesita Provider, pero solo en el cliente

Un store de Zustand es un singleton a nivel de módulo — no requiere envoltorio de contexto, y se puede acceder desde cualquier lugar importando el hook. Pero eso también significa que depende de APIs del navegador y del estado por cliente, por lo tanto:

  • Solo inicializa/utiliza stores de Zustand en Componentes Cliente ('use client' en Next.js).
  • Nunca crees ni leas un store de Zustand en un Componente Servidor — no hay aislamiento "por petición" como ocurre con un QueryClient fresco, por lo que hacerlo corre el riesgo de filtrar estado entre usuarios/peticiones.

3. Organiza stores grandes con el patrón de slices, no con un único bloque plano

Un store único con preocupaciones no relacionadas agrupadas (usuario, carrito, tema de UI, notificaciones, todo en un mismo objeto) se vuelve difícil de navegar y razonar. Divide por dominio en slices, luego compónlos en una única llamada create():

// userSlice.ts
export interface UserSlice {
  user: User | null;
  setUser: (user: User) => void;
  logout: () => void;
}

export const createUserSlice: StateCreator<UserSlice> = (set) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null }),
});

// cartSlice.ts
export interface CartSlice {
  items: CartItem[];
  addItem: (item: CartItem) => void;
}

export const createCartSlice: StateCreator<CartSlice> = (set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
});

// store.ts
type AppState = UserSlice & CartSlice;

export const useAppStore = create<AppState>()((...a) => ({
  ...createUserSlice(...a),
  ...createCartSlice(...a),
}));

Recurre a stores verdaderamente separados (no slices) solo cuando dos dominios son completamente independientes y nunca necesitan interactuar (por ejemplo, un store de autenticación vs. un store solo de analítica) — varios stores añaden sobrecarga de coordinación, así que por defecto usa slices dentro de un único store primero.

4. Usa métodos de acción con nombre, no llamadas directas a set() desde los componentes

Expón acciones como funciones con nombre en el store; no invoques directamente el set del store desde un componente.

// Evitar: funciona, pero no es semántico — los componentes necesitan conocer la forma del estado
const set = useAppStore((state) => state.set);
set({ theme: "dark" });

// Preferir: acciones con nombre y autodocumentadas
const setTheme = useAppStore((state) => state.setTheme);
setTheme("dark");

Usa get() dentro de las acciones de un slice para leer otro estado o invocar acciones de otros slices (por ejemplo, una acción logout que también limpia el carrito) — pero ten cuidado con dependencias circulares entre slices que se llaman entre sí.

5. Selecciona siempre la porción específica de estado que necesitas — nunca el store completo

// Evitar: rerenderiza en CUALQUIER cambio del store, incluso campos no relacionados
const state = useAppStore();

// Preferir: rerenderiza solo cuando `user` cambia
const user = useAppStore((state) => state.user);

6. Usa useShallow al seleccionar múltiples valores o un objeto

Seleccionar un objeto o array recién construido desde el store (p. ej. { user, theme }) crea una nueva referencia en cada render incluso si los valores subyacentes no han cambiado, causando rerenders innecesarios. Envuélvelo en useShallow para que Zustand compare los valores seleccionados de manera superficial en lugar de por referencia:

import { useShallow } from "zustand/react/shallow";

const { user, theme } = useAppStore(
  useShallow((state) => ({ user: state.user, theme: state.theme }))
);

7. Usa el middleware immer para actualizaciones de estado anidadas

Desplegar manualmente múltiples niveles de estado anidado para actualizar un campo es propenso a errores y verboso. El middleware immer permite escribir sintaxis de "mutación" directa que en realidad se aplica de manera inmutable bajo el capó:

import { immer } from "zustand/middleware/immer";

const useStore = create<State>()(
  immer((set) => ({
    nested: { deeply: { value: 0 } },
    increment: () =>
      set((state) => {
        state.nested.deeply.value += 1; // parece mutable, en realidad es inmutable
      }),
  }))
);

Omite immer para actualizaciones de muy alta frecuencia (por ejemplo, estado de animación a 60fps) donde su sobrecarga importa; para estado típico de tipo CRUD es una ganancia de legibilidad que vale el pequeño costo.

8. Tipifica los stores con la forma currificada create<State>()(...)

El create simple ((set) => ({...})) no se puede inferir de manera confiable por TypeScript para nada más allá de un store trivial, debido a cómo el genérico de estado es invariante. Siempre usa la forma currificada:

interface CounterState {
  count: number;
  increment: () => void;
}

const useCounterStore = create<CounterState>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

9. No almacenes valores derivados/computados — calcúlalos en un selector o en el componente

Si un valor se puede calcular a partir de otro estado ya presente en el store (un total a partir de una lista de elementos, un booleano a partir de un campo de estado), no lo almacenes también como su propio campo — se desincronizará en cuanto una ruta de actualización olvide recalcularlo. Calcúlalo en el selector, o como un valor derivado simple en el componente:

// Evitar: un campo `itemCount` separado que debe mantenerse sincronizado manualmente
addItem: (item) => set((state) => ({ items: [...state.items, item], itemCount: state.itemCount + 1 })),

// Preferir: derivarlo en el momento de la lectura
const itemCount = useCartStore((state) => state.items.length);

10. Usa persist para el estado que debe sobrevivir entre sesiones, devtools solo en desarrollo

  • Usa el middleware persist (respaldado por localStorage/sessionStorage, o un adaptador de almacenamiento personalizado) para el estado que genuinamente necesita sobrevivir a una recarga de página o una nueva sesión (contenido del carrito, preferencias del usuario) — no para estado que debería reiniciarse en cada visita.
  • Incluye un version y, si la forma alguna vez cambia, una función migrate en las opciones de persist para que el estado persistido antiguo no rompa la aplicación silenciosamente después de un cambio de esquema.
  • Usa el middleware devtools para integrarse con Redux DevTools e inspeccionar cambios de estado durante el desarrollo, pero asegúrate de que no esté habilitado en compilaciones de producción.
export const useSettingsStore = create<SettingsState>()(
  devtools(
    persist(
      (set) => ({ theme: "light", setTheme: (theme) => set({ theme }) }),
      { name: "settings-storage", version: 1 }
    )
  )
);

11. Usa devtools para rastrear el flujo de estado, y combínalo con React DevTools para detectar rerenders innecesarios

devtools no es solo un registrador agradable de tener — úsalo activamente para depurar por qué cambió el estado, no solo que cambió:

  • Nombra cada acción para que la línea de tiempo de devtools muestre un rastro significativo en lugar de una entrada set anónima para cada actualización. Pasa un tercer argumento a set con un nombre/tipo de acción:

    addItem: (item) =>
      set(
        (state) => ({ items: [...state.items, item] }),
        false,
        "cart/addItem" // aparece como esta etiqueta en la lista de acciones de Redux DevTools
      ),
    
  • Nombra cada store mediante devtools(..., { name: "cart-store" }) cuando un proyecto tiene varios stores, para que el panel de devtools no muestre una mezcla ambigua de actualizaciones de todos los stores a la vez.

  • Usa la línea de tiempo de devtools para confirmar el orden real del flujo de estado cuando un error parezca una condición de carrera o un problema de orden de actualización — recorre la lista de acciones para ver exactamente qué cambió, cuándo, y con qué estado anterior/siguiente, en lugar de adivinar a partir de console.log dispersos en el código de las acciones.

  • Diagnosticar rerenders innecesarios es una herramienta aparte, no algo que devtools muestre por sí mismo. Usa el Perfilador de React DevTools (o why-did-you-render en desarrollo si necesitas detalle a nivel de sitio de llamada) para ver qué componentes rerenderizaron ante un cambio de estado determinado y por qué. Si un componente rerenderiza en una actualización del store que no afecta los campos que selecciona, es una señal de que te falta un selector (regla 5) o un envoltorio useShallow (regla 6) — ve a arreglar el selector, no el store.

12. Haz los stores testables: exporta el creador sin procesar, no solo el hook enlazado

Exporta tanto el hook (para componentes) como el StateCreator/función de creación del store subyacente (para pruebas), para que las pruebas puedan crear una instancia de store nueva y aislada por prueba en lugar de compartir el singleton a nivel de módulo en todo el conjunto de pruebas (lo que filtra estado entre pruebas).

Aplicando estas reglas

  • Nuevo store: comienza con un único store usando el patrón de slices desde el primer día si hay más de un dominio involucrado; tipifícalo con la forma currificada create<State>()(...); añade persist/devtools solo si el estado realmente los necesita.
  • Revisando código existente: marca selección de store completo sin función selectora, llamadas directas a set() desde componentes en lugar de acciones con nombre, estado derivado almacenado como campo propio, datos obtenidos del servidor duplicados en Zustand en lugar de TanStack Query, cualquier store tocado desde un Componente Servidor, acciones/stores sin nombre que hacen que la línea de tiempo de devtools sea difícil de leer, y cualquier componente que rerenderice por cambios de store no relacionados (selector faltante o useShallow).

Skills relacionados