//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Instala Zustand, crea un store con create y consúmelo en cualquier componente sin providers ni envoltorios de context.
npm install zustand// stores/counter-store.ts
import { create } from "zustand";
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));"use client";
import { useCounterStore } from "@/stores/counter-store";
function Counter() {
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
return (
<div>
<p>Recuento: {count}</p>
<button onClick={increment}>+1</button>
</div>
);
}// stores/todo-store.ts
import { create } from "zustand";
interface Todo {
id: string;
text: string;
done: boolean;
}
interface TodoState {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: string) => void;
removeTodo: (id: string) => void;
clearCompleted: () => void;
}
export const useTodoStore = create<TodoState>((set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { id: crypto.randomUUID(), text, done: false }],
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter((t) => t.id !== id),
})),
clearCompleted: () =>
set((state) => ({
todos: state.todos.filter((t) => !t.done),
})),
}));// components/todo-app.tsx
"use client";
import { useState } from "react";
import { useTodoStore } from "@/stores/todo-store";
export function TodoApp() {
const [input, setInput] = useState("");
const todos = useTodoStore((s) => s.todos);
const addTodo = useTodoStore((s) => s.addTodo);
const toggleTodo = useTodoStore((s) => s.toggleTodo);
const removeTodo = useTodoStore((s) => s.removeTodo);
const clearCompleted = useTodoStore((s) => s.clearCompleted);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim()) {
addTodo(input.trim());
setInput("");
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button type="submit">Añadir</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.text}
</span>
</label>
<button onClick={() => removeTodo(todo.id)}>Eliminar</button>
</li>
))}
</ul>
<button onClick={clearCompleted}>Limpiar completados</button>
<p>{todos.filter((t) => !t.done).length} elementos restantes</p>
</div>
);
}create devuelve un hook de React que se suscribe al store. El store en sí es un objeto JavaScript vanilla gestionado fuera de React.set fusiona el state parcial en el state actual (fusión superficial por defecto).set puede aceptar un objeto (fusionado) o una función (state) => partialState (para actualizaciones basadas en el state actual).Obtener el state completo (no recomendado por rendimiento):
const { count, increment } = useCounterStore();
// Re-renderiza ante CUALQUIER cambio de stateUsar el store fuera de React:
// Accede al state directamente (sin hooks)
const count = useCounterStore.getState().count;
// Suscríbete a los cambios
const unsub = useCounterStore.subscribe((state) => {
console.log("El recuento cambió:", state.count);
});Reemplazar el state en lugar de fusionarlo:
set({ count: 0 }, true); // El segundo argumento `true` reemplaza todo el statecreate<State>().set está tipado para aceptar Partial<State> o (state: State) => Partial<State>.import { create, StoreApi } from "zustand";
type Store = StoreApi<CounterState>;useStore() sin selector) hace que el componente se re-renderice ante cada cambio de state. Usa siempre selectores.set realiza una fusión superficial. Los objetos anidados deben propagarse manualmente: set({ user: { ...state.user, name: "new" } }).set es sincrónico. La actualización del state y el re-renderizado ocurren en el mismo tick (agrupados por React 18+).| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Zustand | Sin providers, API mínima, selectores rápidos | Singleton en SSR, curva de aprendizaje para middleware |
| React Context | Integrado, sin dependencias | Re-renderiza a todos los consumidores, sin selectores |
| Redux Toolkit | Ecosistema maduro, DevTools | Boilerplate, configuración compleja |
| Jotai | Modelo atómico, bottom-up | Modelo mental distinto, muchos átomos que gestionar |
De una aplicación SaaS en producción con Next.js 15 / React 19 (SystemsArchitect.io).
// Ejemplo en producción: store de auth
// File: src/stores/auth.ts
import { create } from 'zustand'
import { User } from '@supabase/supabase-js'
interface AuthState {
user: User | null
loading: boolean
setUser: (user: User | null) => void
setLoading: (loading: boolean) => void
}
export const useAuthStore = create<AuthState>()((set) => ({
user: null,
loading: true,
setUser: (user) => set({ user }),
setLoading: (loading) => set({ loading }),
}))
// Uso con selector (evita re-renderizados por cambios de state no relacionados):
// const user = useAuthStore((state) => state.user)
// NO: const { user } = useAuthStore() // se suscribe a TODOS los cambiosLo que esto demuestra en producción:
create<AuthState>()((set) => ...) no son un error tipográfico. El primer () es obligatorio al usar genéricos de TypeScript con Zustand, y también habilita el encadenamiento de middleware (p. ej., create<AuthState>()(persist(devtools((set) => ...)))).set({ user }) hace una fusión superficial, no un reemplazo. Solo se actualiza el campo user mientras loading permanece intacto. Este es el comportamiento predeterminado de Zustand.useAuthStore((s) => s.user) se suscribe solo al campo user. Usar const { user } = useAuthStore() sin selector se suscribe a todo el store, provocando re-renderizados cada vez que cambia cualquier campo (incluido loading).useAuthStore.getState(), incluso en código no React como utilidades de API o middleware.create devuelve un hook de React (p. ej., useCounterStore) que se suscribe al store.useCounterStore((state) => state.count).(state) => state.count se suscriben solo a ese slice de state.const { count } = useStore()) se suscribe a todo el store, provocando re-renderizados ante cada cambio.set realiza una fusión superficial del state parcial en el state actual.true como segundo argumento: set({ count: 0 }, true).// Lee el state directamente
const count = useCounterStore.getState().count;
// Suscríbete a los cambios
const unsub = useCounterStore.subscribe((state) => {
console.log("Recuento:", state.count);
});set({ count: 0 }) establece un valor estático independientemente del state actual.set((state) => ...) calcula el siguiente state basándose en el state actual, lo cual es necesario para incrementos, toggles y actualizaciones derivadas.useStore() sin selector se suscribe a cada propiedad del store.set hace una fusión superficial solo en el nivel superior.set((s) => ({ user: { ...s.user, name: "new" } })).interface CounterState {
count: number;
increment: () => void;
}
const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}));create<State>().set acepta Partial<State> o (state: State) => Partial<State>.() es obligatorio al usar genéricos de TypeScript con middleware.create<State>()(persist(devtools((set) => ...))).create<State>((set) => ...) (una sola invocación) es suficiente.set es sincrónico. La actualización del state ocurre de inmediato.set pueden agruparse en un solo renderizado.Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥