En esta página

Estado Global con Zustand

14 min lectura TextoCap. 5 — Producción

¿Por qué Zustand?

Zustand es una librería de gestión de estado minimalista para React con una API sorprendentemente simple. En comparación con Redux, no requiere reducers, actions, action creators ni dispatch. Solo defines un store con estado y acciones, y listo.

npm install zustand

Tu primer store

import { create } from 'zustand';

interface ContadorStore {
  conteo: number;
  incrementar: () => void;
  decrementar: () => void;
  reiniciar: () => void;
  incrementarPor: (cantidad: number) => void;
}

const useContador = create<ContadorStore>((set) => ({
  conteo: 0,
  incrementar: () => set((state) => ({ conteo: state.conteo + 1 })),
  decrementar: () => set((state) => ({ conteo: Math.max(0, state.conteo - 1) })),
  reiniciar: () => set({ conteo: 0 }),
  incrementarPor: (cantidad) => set((state) => ({ conteo: state.conteo + cantidad })),
}));

function Contador(): React.JSX.Element {
  // Selección granular: solo re-renderiza cuando conteo cambia
  const conteo = useContador((s) => s.conteo);
  const incrementar = useContador((s) => s.incrementar);
  const decrementar = useContador((s) => s.decrementar);

  return (
    <div>
      <button type="button" onClick={decrementar}>−</button>
      <span>{conteo}</span>
      <button type="button" onClick={incrementar}>+</button>
    </div>
  );
}

Selectores — el patrón de rendimiento clave

El selector es una función que extrae solo la parte del store que el componente necesita:

// ❌ Se re-renderiza cuando CUALQUIER cosa en el store cambia
const storeCompleto = useAppStore();

// ✅ Solo se re-renderiza cuando cambia el nombre
const nombre = useAppStore((s) => s.usuario?.nombre);

// ✅ Múltiples valores: usa shallow para comparación correcta
import { shallow } from 'zustand/shallow';
const { nombre, email } = useAppStore(
  (s) => ({ nombre: s.usuario?.nombre, email: s.usuario?.email }),
  shallow
);

Estado asíncrono en Zustand

interface ProductosStore {
  productos: Producto[];
  cargando: boolean;
  error: string | null;
  cargarProductos: () => Promise<void>;
  agregarProducto: (p: Omit<Producto, 'id'>) => Promise<void>;
}

const useProductos = create<ProductosStore>((set, get) => ({
  productos: [],
  cargando: false,
  error: null,

  cargarProductos: async () => {
    set({ cargando: true, error: null });
    try {
      const res = await fetch('/api/productos');
      if (!res.ok) throw new Error(`Error ${res.status}`);
      const productos = await res.json() as Producto[];
      set({ productos, cargando: false });
    } catch (err) {
      set({
        cargando: false,
        error: err instanceof Error ? err.message : 'Error desconocido',
      });
    }
  },

  agregarProducto: async (producto) => {
    const res = await fetch('/api/productos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(producto),
    });
    const nuevo = await res.json() as Producto;
    // Actualización optimista: agregar al store sin esperar revalidación
    set((state) => ({ productos: [...state.productos, nuevo] }));
  },
}));

Middleware devtools

El middleware devtools conecta Zustand con Redux DevTools Extension:

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useStore = create<MiStore>()(
  devtools(
    (set) => ({
      // tu store aquí
    }),
    { name: 'MiAplicacion', enabled: process.env.NODE_ENV !== 'production' }
  )
);

Con devtools puedes ver el historial de acciones, inspeccionar el estado en cada momento y hacer time-travel debugging.

Comparación con Redux Toolkit

// Redux Toolkit — más verboso pero más convenciones
const contadorSlice = createSlice({
  name: 'contador',
  initialState: { valor: 0 },
  reducers: {
    incrementar: (state) => { state.valor += 1; },
    decrementar: (state) => { state.valor -= 1; },
  },
});

// Zustand — más simple, misma funcionalidad
const useContador = create((set) => ({
  valor: 0,
  incrementar: () => set((s) => ({ valor: s.valor + 1 })),
  decrementar: () => set((s) => ({ valor: s.valor - 1 })),
}));
Aspecto Zustand Redux Toolkit
Setup Mínimo Moderado
Boilerplate Muy poco Poco (mejorado en RTK)
TypeScript Excelente Excelente
DevTools Sí (middleware) Sí (nativo)
Middleware Flexible Thunk, Saga, RTK Query
Bundle size ~1KB ~12KB
Curva aprendizaje Baja Media

Zustand es la elección ideal para proyectos pequeños y medianos. Para equipos grandes o proyectos enterprise con convenciones estrictas, Redux Toolkit puede ser la mejor opción.

Usa selectores para evitar re-renders innecesarios
En lugar de useStore() que suscribe al store completo, usa useStore(s => s.campo) para suscribirte solo a lo que necesitas. Si el componente solo usa el nombre del usuario, solo se re-renderizará cuando el nombre cambie, no cuando cambie cualquier otra parte del store.
Zustand vs Redux Toolkit — cuándo usar cada uno
Zustand es más simple (menos boilerplate, sin actions/reducers/dispatch), ideal para la mayoría de proyectos. Redux Toolkit brilla en aplicaciones muy grandes con equipos grandes que necesitan convenciones estrictas, time-travel debugging avanzado y ecosistema maduro de middleware.
El middleware persist serializa el estado en localStorage
persist de Zustand guarda el estado en localStorage (o sessionStorage). Solo persiste datos serializables: no funciones, clases, Dates o Maps directamente. Usa partialize para especificar qué parte del estado persistir y evita guardar datos sensibles del usuario en localStorage.