¿Qué es Zustand?
Zustand es un almacén de estado pequeño y rápido para React. Toda su API es sumamente sencilla: llamas a create con una función que devuelve el estado y las acciones, y obtienes un hook. No hay provider, ni reducer, ni tipos de acción, ni dispatch. Simplemente importas el hook y lo usas.
Su nombre significa “estado” en alemán, y la librería hace honor a esa simplicidad. Pesa alrededor de un kilobyte, utiliza suscripciones basadas en selectores para mantener los re-renders al mínimo y funciona con JavaScript y TypeScript puro. Para muchas aplicaciones de React, es la opción con menos ceremonia posible para compartir el estado de forma global.
Creando un store
Un store es un hook. El estado y las funciones que lo actualizan conviven en el mismo lugar.
// store.js
import { create } from "zustand";
export const useStore = create((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
double: () => get().count * 2,
}));
set fusiona el objeto devuelto en el estado, y get lee el estado actual de forma imperativa. No hay que escribir ningún reducer ni despachar ninguna acción; simplemente son funciones que actualizan el estado.
Lectura del estado con selectores
Los componentes se suscriben exactamente a la parte del estado que necesitan.
// Counter.jsx
import { useStore } from "./store";
export function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}
Debido a que el componente se suscribe únicamente a state.count, las actualizaciones en partes no relacionadas del store no provocan que se vuelva a renderizar. Esa es la diferencia clave de rendimiento respecto a la Context API de React, que renderiza nuevamente a cada consumidor cuando el valor cambia.
Para seleccionar múltiples valores, utiliza una comparación superficial (shallow comparison) para que el componente se renderice solo cuando uno de ellos cambie realmente:
// shallow.js
import { useShallow } from "zustand/react/shallow";
const { count, increment } = useStore(
useShallow((state) => ({ count: state.count, increment: state.increment })),
);
Actualización del estado
set realiza una fusión (merge) por defecto, por lo que solo necesitas especificar qué ha cambiado. Pasa una función cuando el siguiente valor dependa del anterior.
// updates.js
set({ count: 0 }); // merge
set((state) => ({ count: state.count + 1 })); // derive from previous
set((state) => ({ items: [...state.items, item] })); // arrays
En el caso de objetos anidados, set reemplaza la clave de nivel superior, así que utiliza el operador spread con el objeto existente cuando necesites conservar otras propiedades. El middleware Immer está disponible si prefieres una sintaxis de mutación para actualizaciones profundas.
Acciones asíncronas
La lógica asíncrona es simplemente una función async en el store. No hay ningún middleware que configurar.
// usersStore.js
import { create } from "zustand";
export const useUsers = create((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const res = await fetch("/api/users");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
set({ users: await res.json(), loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
}));
Los flags de carga (loading) y error residen junto a los datos, y los componentes seleccionan únicamente las piezas que necesitan mostrar.
Slices y middleware
Para un store extenso, divídelo en slices que se combinen en un único store. Cada slice es una función que recibe set y get.
// slices/cart.js
export const createCartSlice = (set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
});
// store.js
import { create } from "zustand";
import { createCartSlice } from "./slices/cart";
import { createUserSlice } from "./slices/user";
export const useStore = create((...args) => ({
...createCartSlice(...args),
...createUserSlice(...args),
}));
El middleware se compone como funciones alrededor del creador del store. Los más comunes son:
- persist guarda y rehidrata el estado en
localStorageu otro almacenamiento. - devtools conecta el store con la extensión Redux DevTools.
- immer te permite escribir actualizaciones mutables que se mantienen inmutables.
// persisted.js
import { persist } from "zustand/middleware";
export const useSettings = create(
persist(
(set) => ({
theme: "dark",
setTheme: (theme) => set({ theme }),
}),
{ name: "settings" },
),
);
Cuándo usar Zustand
Zustand es ideal para el estado global del cliente: temas y preferencias, un carrito de compras, estado de la UI compartido entre componentes distantes, borradores, filtros y el estado de los modales. Es especialmente útil cuando Context provocaría demasiados re-renders o cuando Redux se siente como una estructura más compleja de lo que necesitas.
No lo utilices para el estado del servidor. Los datos que provienen de una API requieren almacenamiento en caché, deduplicación, refetching en segundo plano e invalidación, y TanStack Query gestiona esas necesidades mucho mejor. La configuración más común es utilizar Zustand para el estado del cliente y TanStack Query para el estado del servidor.
Mejores prácticas
- Lee siempre el estado mediante un selector.
- Utiliza
useShallowcuando selecciones múltiples valores. - Mantén las acciones dentro del store para que los componentes no necesiten conocer la estructura del estado.
- Divide los stores grandes en slices.
- Utiliza el persist middleware para los ajustes que deban persistir tras recargar la página.
- Mantén los datos del servidor fuera del store.
- Almacena datos planos y serializables siempre que sea posible.
Errores comunes
- Llamar a
useStore()sin un selector y provocar un re-renderizado en cada cambio. - Retornar un nuevo objeto desde un selector sin realizar una comparación superficial (shallow comparison).
- Tratar Zustand como un caché de estado del servidor.
- Mutar el estado anidado sin Immer y perder las actualizaciones.
- Crear un store dentro de un componente, lo que provoca que se reinicie en cada renderizado.
- Añadir la complejidad y rituales típicos de Redux que Zustand fue diseñado para eliminar.
Próximos pasos
Zustand suele ser el punto medio ideal entre Context y Redux. Compáralo con Context para un intercambio de datos sencillo y con Redux Toolkit para un estado más estricto y robusto, y traslada los datos del servidor a TanStack Query. En Vue, el mismo rol lo cumple Pinia.