Zustand se ha consolidado en 2026 como la solución de gestión de estado más popular en el ecosistema React. Ligero, intuitivo, sin boilerplate y con TypeScript de primera clase: Zustand es lo que Redux debería haber sido desde el principio. En esta guía completa aprenderás a usar Zustand desde cero junto con los patrones de estado más avanzados para construir aplicaciones React profesionales.

¿Por qué Zustand? El problema del estado global en React

Gestionar el estado global en React siempre ha sido un desafío. El Context API de React es suficiente para casos simples, pero tiene un problema de rendimiento: cuando el contexto cambia, todos los componentes que consumen ese contexto se re-renderizan, aunque no usen el dato que cambió. Redux resuelve esto pero a costa de una cantidad de boilerplate que puede resultar abrumadora.

Zustand aparece con una propuesta diferente: un store global basado en hooks, con selectores atómicos, sin providers y con una API tan sencilla que puedes aprender lo esencial en menos de 10 minutos. Y encima, el rendimiento es excelente porque los componentes solo se re-renderizan cuando cambia exactamente el dato que están consumiendo.

CaracterísticaZustandRedux ToolkitContext APIJotai
Tamaño del bundle⚡ ~1KB🔴 ~15KB✅ 0KB (nativo)⚡ ~3KB
Boilerplate✅ Mínimo⚠️ Medio✅ Mínimo✅ Mínimo
TypeScript✅ Excelente✅ Excelente⚠️ Manual✅ Excelente
DevTools✅ Redux DevTools✅ Redux DevTools❌ No✅ Jotai DevTools
Rendimiento⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Curva de aprendizaje✅ Muy baja⚠️ Media✅ Baja✅ Baja

Instalación y tu primer store

npm install zustand

El store más sencillo posible, un contador:

// src/stores/contadorStore.ts
import { create } from 'zustand';

interface ContadorState {
  cuenta: number;
  incrementar: () => void;
  decrementar: () => void;
  resetear: () => void;
  establecer: (valor: number) => void;
}

export const useContadorStore = create((set) => ({
  // Estado inicial
  cuenta: 0,

  // Acciones
  incrementar: () => set((state) => ({ cuenta: state.cuenta + 1 })),
  decrementar: () => set((state) => ({ cuenta: state.cuenta - 1 })),
  resetear: () => set({ cuenta: 0 }),
  establecer: (valor) => set({ cuenta: valor }),
}));

// Uso en cualquier componente (sin Provider, sin contexto)
import { useContadorStore } from './stores/contadorStore';

function Contador() {
  const cuenta = useContadorStore((state) => state.cuenta);
  const incrementar = useContadorStore((state) => state.incrementar);
  const resetear = useContadorStore((state) => state.resetear);

  return (
    

Cuenta: {cuenta}

); }

Fíjate en algo crucial: no hay ningún Provider que envuelva la aplicación. El store de Zustand es un módulo JavaScript normal que cualquier componente puede importar y usar directamente. Y gracias al selector (state) => state.cuenta, el componente solo se re-renderiza cuando cuenta cambia, no cuando cambia cualquier otra parte del store.

Store completo: gestión del carrito de compra

Veamos un ejemplo más realista: un store para gestionar el carrito de una tienda online.

// src/stores/carritoStore.ts
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';

interface Producto {
  id: number;
  nombre: string;
  precio: number;
  imagen?: string;
}

interface ItemCarrito extends Producto {
  cantidad: number;
}

interface CarritoState {
  items: ItemCarrito[];
  // Getters computados
  totalItems: () => number;
  totalPrecio: () => number;
  estaEnCarrito: (id: number) => boolean;
  // Acciones
  agregar: (producto: Producto) => void;
  eliminar: (id: number) => void;
  cambiarCantidad: (id: number, cantidad: number) => void;
  vaciar: () => void;
}

export const useCarritoStore = create()(
  // devtools: integración con Redux DevTools
  devtools(
    // persist: guarda el estado en localStorage automáticamente
    persist(
      (set, get) => ({
        items: [],

        // Getters: funciones que calculan valores derivados
        totalItems: () => get().items.reduce((acc, item) => acc + item.cantidad, 0),
        totalPrecio: () => get().items.reduce((acc, item) => acc + item.precio * item.cantidad, 0),
        estaEnCarrito: (id) => get().items.some((item) => item.id === id),

        // Acciones
        agregar: (producto) => {
          set((state) => {
            const itemExistente = state.items.find((i) => i.id === producto.id);
            if (itemExistente) {
              // Si ya existe, incrementa la cantidad
              return {
                items: state.items.map((i) =>
                  i.id === producto.id ? { ...i, cantidad: i.cantidad + 1 } : i
                ),
              };
            }
            // Si no existe, añade con cantidad 1
            return { items: [...state.items, { ...producto, cantidad: 1 }] };
          });
        },

        eliminar: (id) =>
          set((state) => ({ items: state.items.filter((i) => i.id !== id) })),

        cambiarCantidad: (id, cantidad) => {
          if (cantidad <= 0) {
            get().eliminar(id);
            return;
          }
          set((state) => ({
            items: state.items.map((i) => (i.id === id ? { ...i, cantidad } : i)),
          }));
        },

        vaciar: () => set({ items: [] }),
      }),
      {
        name: 'carrito-storage', // Clave en localStorage
        // Solo persistir los items, no las funciones
        partialize: (state) => ({ items: state.items }),
      }
    ),
    { name: 'CarritoStore' } // Nombre en Redux DevTools
  )
);

// Hook con selector optimizado para el badge del carrito
export const useTotalItems = () => useCarritoStore((state) => state.totalItems());

Middlewares de Zustand: persist, devtools e immer

Zustand tiene un sistema de middlewares muy potente. Los más útiles son:

// Usando immer para mutaciones directas (más legible para estados anidados)
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

interface State {
  usuario: { nombre: string; preferencias: { tema: 'claro' | 'oscuro'; idioma: string } };
  actualizarTema: (tema: 'claro' | 'oscuro') => void;
}

const useStore = create()(
  immer((set) => ({
    usuario: {
      nombre: 'Adam',
      preferencias: { tema: 'oscuro', idioma: 'es' },
    },
    // Con immer puedes mutar directamente sin spread
    actualizarTema: (tema) =>
      set((state) => {
        state.usuario.preferencias.tema = tema; // ¡Mutación directa!
      }),
  }))
);

Patrones avanzados: stores asíncronos y fetching de datos

// src/stores/productosStore.ts
import { create } from 'zustand';

type EstadoCarga = 'idle' | 'cargando' | 'exito' | 'error';

interface Producto { id: number; nombre: string; precio: number; }

interface ProductosState {
  productos: Producto[];
  estado: EstadoCarga;
  error: string | null;
  paginaActual: number;
  totalPaginas: number;
  // Acciones
  cargarProductos: (pagina?: number) => Promise;
  buscarProductos: (query: string) => Promise;
}

export const useProductosStore = create((set, get) => ({
  productos: [],
  estado: 'idle',
  error: null,
  paginaActual: 1,
  totalPaginas: 1,

  cargarProductos: async (pagina = 1) => {
    set({ estado: 'cargando', error: null });
    try {
      const res = await fetch('/api/productos?pagina=' + pagina);
      if (!res.ok) throw new Error('Error al cargar productos');
      const { datos, totalPaginas } = await res.json();
      set({ productos: datos, estado: 'exito', paginaActual: pagina, totalPaginas });
    } catch (err) {
      set({ estado: 'error', error: err instanceof Error ? err.message : 'Error desconocido' });
    }
  },

  buscarProductos: async (query) => {
    if (!query.trim()) {
      get().cargarProductos();
      return;
    }
    set({ estado: 'cargando' });
    try {
      const res = await fetch('/api/productos?busqueda=' + encodeURIComponent(query));
      const { datos } = await res.json();
      set({ productos: datos, estado: 'exito' });
    } catch {
      set({ estado: 'error', error: 'Error en la búsqueda' });
    }
  },
}));

// Componente que usa el store asíncrono
function ListaProductos() {
  const { productos, estado, error, cargarProductos } = useProductosStore();

  useEffect(() => { cargarProductos(); }, []);

  if (estado === 'cargando') return 

Cargando...

; if (estado === 'error') return

Error: {error}

; return (
    {productos.map((p) =>
  • {p.nombre} - {p.precio}€
  • )}
); }

Separar el store en slices para proyectos grandes

Para proyectos grandes, se recomienda dividir el store en «slices» (segmentos) y combinarlos. Esto mejora la organización y permite reutilizar la lógica:

// src/stores/slices/authSlice.ts
import { StateCreator } from 'zustand';

export interface AuthSlice {
  usuario: { id: number; nombre: string } | null;
  estaAutenticado: boolean;
  login: (email: string, pass: string) => Promise;
  logout: () => void;
}

export const createAuthSlice: StateCreator = (set) => ({
  usuario: null,
  estaAutenticado: false,
  login: async (email, pass) => {
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify({ email, password: pass }),
      headers: { 'Content-Type': 'application/json' },
    });
    const { user } = await res.json();
    set({ usuario: user, estaAutenticado: true });
  },
  logout: () => set({ usuario: null, estaAutenticado: false }),
});

// src/stores/slices/uiSlice.ts
export interface UISlice {
  sidebarAbierto: boolean;
  tema: 'claro' | 'oscuro';
  toggleSidebar: () => void;
  cambiarTema: () => void;
}

export const createUISlice: StateCreator = (set) => ({
  sidebarAbierto: false,
  tema: 'oscuro',
  toggleSidebar: () => set((s) => ({ sidebarAbierto: !s.sidebarAbierto })),
  cambiarTema: () => set((s) => ({ tema: s.tema === 'claro' ? 'oscuro' : 'claro' })),
});

// src/stores/appStore.ts - Store combinado
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { AuthSlice, createAuthSlice } from './slices/authSlice';
import { UISlice, createUISlice } from './slices/uiSlice';

type AppStore = AuthSlice & UISlice;

export const useAppStore = create()(
  devtools(
    persist(
      (...args) => ({
        ...createAuthSlice(...args),
        ...createUISlice(...args),
      }),
      { name: 'app-store', partialize: (s) => ({ tema: s.tema }) }
    )
  )
);

Testing de stores con Zustand

// src/stores/__tests__/carritoStore.test.ts
import { act, renderHook } from '@testing-library/react';
import { useCarritoStore } from '../carritoStore';

// Resetear el store entre tests
beforeEach(() => {
  useCarritoStore.setState({ items: [] });
});

describe('CarritoStore', () => {
  const producto = { id: 1, nombre: 'Teclado', precio: 89.99 };

  test('debe agregar un producto al carrito', () => {
    const { result } = renderHook(() => useCarritoStore());
    act(() => { result.current.agregar(producto); });
    expect(result.current.items).toHaveLength(1);
    expect(result.current.items[0].cantidad).toBe(1);
  });

  test('debe incrementar la cantidad si el producto ya existe', () => {
    const { result } = renderHook(() => useCarritoStore());
    act(() => {
      result.current.agregar(producto);
      result.current.agregar(producto);
    });
    expect(result.current.items).toHaveLength(1);
    expect(result.current.items[0].cantidad).toBe(2);
  });

  test('debe calcular el total correctamente', () => {
    const { result } = renderHook(() => useCarritoStore());
    act(() => { result.current.agregar(producto); });
    expect(result.current.totalPrecio()).toBeCloseTo(89.99);
  });
});

Conclusión

Zustand es la prueba de que la gestión de estado global en React no tiene por qué ser complicada. Con una API mínima, rendimiento excelente, TypeScript de primera clase y middlewares potentes como persist y devtools, cubre el 95% de los casos de uso sin el overhead de Redux.

Para la mayoría de proyectos React en 2026, la combinación de estado local con useState/useReducer para UI simple y Zustand para estado global compartido es la arquitectura más pragmática y mantenible. Si todavía no lo has probado, hazlo en tu próximo proyecto. Creo que no querrás volver atrás. ¿Qué solución de estado usas actualmente en tus proyectos? Déjame tu respuesta en los comentarios.