Conceptos de Zustand
En la lección anterior vimos por qué compartir estado con props o con Context se vuelve incómodo. Zustand (palabra alemana para "estado") es una librería de estado global para React que resuelve ese problema con una idea muy corta: un store es una función que devuelve un hook.
Si ya conoces Redux Toolkit (lo estudiamos en Computación en Internet 3), te sorprenderá lo poco que hay que aprender: no existen dispatch, ni objetos action, ni reducers, ni Provider.
1. Qué es Zustand
Zustand es una librería pequeña, sin dependencias y sin opiniones fuertes sobre cómo organizar tu código. Su modelo mental cabe en cuatro frases:
- Creas un store con la función
create. - El store guarda estado y acciones en el mismo objeto.
- Las acciones cambian el estado llamando a
set. - Los componentes leen con el hook que
createdevuelve, indicando con un selector qué dato necesitan.
Usamos Zustand 5 con React 19 y Next.js 16. Si encuentras tutoriales antiguos (versión 3 o 4), la mayoría del código sigue funcionando; la diferencia que más afecta se explica en Capacidades de Zustand.
2. Anatomía de un store
Este es el store de notificaciones que usaremos durante la semana. Léelo completo una vez; después lo desarmamos pieza por pieza.
import { create } from 'zustand';
export type NotificationItem = {
id: number;
title: string;
read: boolean;
};
type NotificationsState = {
// Estado
notifications: NotificationItem[];
// Acciones
addNotification: (title: string) => void;
markAllRead: () => void;
};
export const useNotificationsStore = create<NotificationsState>()((set) => ({
notifications: [],
addNotification: (title) =>
set((state) => ({
notifications: [
{ id: Date.now(), title, read: false },
...state.notifications,
],
})),
markAllRead: () =>
set((state) => ({
notifications: state.notifications.map((n) => ({ ...n, read: true })),
})),
}));
create
create recibe una función que describe el store y devuelve un hook (por convención se nombra useAlgoStore). Ese hook es lo único que importan los componentes.
Con TypeScript escribimos create<Estado>()(...): primero indicamos el tipo y luego pasamos la función. Esta forma "curried" es necesaria cuando usamos middleware (lo veremos en la lección 4), así que la usamos siempre para no cambiarla después.
Estado y acciones juntos
A diferencia de Redux, donde el estado vive en un slice y las acciones se despachan desde otro lado, en Zustand el estado y las funciones que lo modifican son propiedades del mismo objeto. Una acción es simplemente una función.
set: cambiar el estado
set es el único camino para modificar el estado. Tiene dos formas:
// 1. Con un objeto: cuando el nuevo valor no depende del anterior
set({ notifications: [] });
// 2. Con una función: cuando depende del estado actual (lo más habitual)
set((state) => ({ notifications: [...state.notifications, nueva] }));
Dos reglas que conviene grabar:
setfusiona de forma superficial (shallow merge): solo debes devolver las propiedades que cambian. Las demás se conservan. No necesitas escribir...state.- El estado es inmutable: nunca hagas
state.notifications.push(...). Devuelve un arreglo nuevo ([...],map,filter). Más adelante veremosimmer, que permite escribir "mutando" sin romper la regla.
get: leer el estado dentro de una acción
El segundo parámetro de la función de create es get, que devuelve el estado actual. Sirve cuando una acción necesita leer algo antes de decidir:
export const useAuthStore = create<AuthState>()((set, get) => ({
token: null,
isAuthenticated: () => get().token !== null,
}));
Selector: qué parte del store necesito
Un selector es una función que recibe el estado completo y devuelve solo lo que el componente necesita:
const notifications = useNotificationsStore((s) => s.notifications);
const addNotification = useNotificationsStore((s) => s.addNotification);
El componente se suscribe solo a ese valor. Si cambia otra parte del store, no se vuelve a renderizar. Por eso los selectores no son opcionales en la práctica.
const store = useNotificationsStore() funciona, pero suscribe el componente a todo el store: se renderiza ante cualquier cambio, y perdemos la ventaja principal frente a Context.
3. El flujo de datos
El recorrido de un cambio en Zustand es más corto que en Redux. Sigue paso a paso qué ocurre cuando el usuario emite una notificación:
Resumen del recorrido:
- El componente llama la acción directamente (
addNotification("Hola")). No haydispatchni objeto action. - La acción llama a
setcon el cambio. - El store reemplaza su estado y avisa a los suscriptores.
- Cada selector compara su resultado anterior con el nuevo (con
Object.is). - Solo los componentes cuyo selector devolvió un valor distinto se vuelven a renderizar.
NotificationCreator solo selecciona la acción addNotification. Las funciones de un store se crean una sola vez, así que su referencia nunca cambia y ese componente no se entera de que la lista creció. Es una de las razones por las que separar "leer datos" de "llamar acciones" con selectores distintos es barato y limpia el rendimiento.
4. Zustand fuera de React
El hook que devuelve create trae, además, tres funciones que funcionan sin componentes. Esto es útil para el cliente HTTP, una utilidad o una prueba:
// Leer el estado actual (sin suscribirse)
const { notifications } = useNotificationsStore.getState();
// Modificarlo desde cualquier archivo
useNotificationsStore.getState().addNotification('Guardado');
useNotificationsStore.setState({ notifications: [] });
// Escuchar cambios (devuelve la función para cancelar la suscripción)
const unsubscribe = useNotificationsStore.subscribe((state) => {
console.log('Ahora hay', state.notifications.length);
});
Un ejemplo clásico: el apiFetch de la semana 12 puede leer el token con useAuthStore.getState().token sin ser un componente ni un hook.
5. Comparativa: Context, Zustand y Redux Toolkit
Ninguna herramienta es la mejor en todos los escenarios. Esta es la misma comparativa que vimos en Redux, ahora con Zustand en el centro:
| Criterio | React Context | Zustand | Redux Toolkit |
|---|---|---|---|
| Piezas que aprendes | createContext, Provider, useContext | create, set, selector | configureStore, slice, reducer, action, dispatch, Provider, selector |
| Boilerplate | Medio | Mínimo | Moderado |
Provider obligatorio | Sí | No (en la versión simple) | Sí |
| Re-renders | A todos los consumidores | Por selector | Por selector |
| Flujo de datos | Libre | Libre (tú decides la estructura) | Estricto y unidireccional |
| Efectos asíncronos | Tú lo resuelves | Acciones async normales | Thunks / createAsyncThunk |
| Herramientas de depuración | React DevTools | Middleware devtools (Redux DevTools) | Redux DevTools nativo |
| Convención impuesta al equipo | Ninguna | Ninguna | Alta |
| Cuándo encaja | Datos casi estáticos (tema, idioma) | Apps pequeñas y medianas, estado de UI y sesión | Equipos grandes, dominios complejos, trazabilidad |
Mismo cambio, dos herramientas
Agregar una notificación en Redux Toolkit y en Zustand:
// slice
reducers: { add: (state, action: PayloadAction<string>) => { state.items.unshift(...) } }
// componente
const dispatch = useAppDispatch();
dispatch(add('Hola'));
const items = useAppSelector((s) => s.notifications.items);
// store
addNotification: (title) => set((s) => ({ notifications: [...] }))
// componente
const addNotification = useNotificationsStore((s) => s.addNotification);
addNotification('Hola');
const notifications = useNotificationsStore((s) => s.notifications);
Que Zustand no imponga estructura es su ventaja y su riesgo. En un equipo grande, sin acuerdos se terminan teniendo veinte formas distintas de escribir un store. Por eso esta semana también fijamos dónde viven los stores y cómo se nombran (lección siguiente).