Zustand
Una solución de gestión de estado pequeña, rápida y escalable con mínimo código repetitivo
Zustand - Lo Esencial para el Estado
Zustand (alemán para "estado") es una solución de gestión de estado pequeña, rápida y escalable. Tiene una API simple basada en hooks, no requiere providers y funciona con el modo concurrente de React. Zustand es perfecto para quienes quieren el poder de Redux sin el código repetitivo.
¿Por Qué Zustand?
- Minimal — Tamaño de paquete diminuto (~1KB)
- Sin Providers — No se necesitan componentes wrapper
- Basado en Hooks — API de hooks simple
- TypeScript — Soporte de primera clase para TypeScript
- Flexible — Funciona fuera de React también
Instalación
npm install zustand
Store Básico
import { create } from 'zustand';
// Crea un store con estado y acciones
const useStore = create((set) => ({
// Estado
count: 0,
// Acciones
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
setCount: (value) => set({ count: value }),
}));
// Usa en componentes - ¡no se necesita Provider!
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
const decrement = useStore((state) => state.decrement);
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
// O desestructura múltiples valores (causa re-render cuando cualquiera cambia)
function CounterAlt() {
const { count, increment, decrement } = useStore();
return (/* mismo JSX */);
}
Ejemplo de App de Tareas
import { create } from 'zustand';
const useTodoStore = create((set, get) => ({
todos: [],
filter: 'all', // 'all' | 'active' | 'completed'
// Acciones
addTodo: (text) => set((state) => ({
todos: [...state.todos, {
id: Date.now(),
text,
completed: false,
}],
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id),
})),
setFilter: (filter) => set({ filter }),
// Valores computados usando get()
getFilteredTodos: () => {
const { todos, filter } = get();
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
},
clearCompleted: () => set((state) => ({
todos: state.todos.filter(todo => !todo.completed),
})),
}));
// Componentes
function TodoList() {
const todos = useTodoStore((state) => state.getFilteredTodos());
const toggleTodo = useTodoStore((state) => state.toggleTodo);
const deleteTodo = useTodoStore((state) => state.deleteTodo);
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Eliminar</button>
</li>
))}
</ul>
);
}
Acciones Asíncronas
const useUserStore = create((set, get) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/users');
const users = await response.json();
set({ users, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
fetchUserById: async (id) => {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
// Combina con usuarios existentes
set((state) => ({
users: state.users.some(u => u.id === id)
? state.users.map(u => u.id === id ? user : u)
: [...state.users, user]
}));
return user;
},
}));
// Uso
function UsersList() {
const { users, loading, error, fetchUsers } = useUserStore();
useEffect(() => {
fetchUsers();
}, []);
if (loading) return <div>Cargando...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Middleware - Persist, DevTools, Immer
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
// Persistir en localStorage
const useStore = create(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{
name: 'app-settings', // clave de localStorage
}
)
);
// Integración con DevTools
const useDebugStore = create(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'),
}),
{ name: 'MyStore' }
)
);
// Immer para actualizaciones inmutables
const useTodoStore = create(
immer((set) => ({
todos: [],
addTodo: (text) => set((state) => {
// ¡Puedes "mutar" con Immer!
state.todos.push({ id: Date.now(), text, completed: false });
}),
toggleTodo: (id) => set((state) => {
const todo = state.todos.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}),
}))
);
// Combina múltiples middleware
const useAdvancedStore = create(
devtools(
persist(
immer((set) => ({
// ... estado y acciones
})),
{ name: 'advanced-store' }
)
)
);
Soporte para TypeScript
import { create } from 'zustand';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
addTodo: (text: string) => void;
toggleTodo: (id: number) => void;
deleteTodo: (id: number) => void;
}
const useTodoStore = create<TodoState>()((set) => ({
todos: [],
addTodo: (text) => set((state) => ({
todos: [...state.todos, { id: Date.now(), text, completed: false }],
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
})),
deleteTodo: (id) => set((state) => ({
todos: state.todos.filter(todo => todo.id !== id),
})),
}));
Zustand vs Redux
| Característica | Zustand | Redux |
|---|---|---|
| Tamaño del bundle | ~1KB | ~10KB+ |
| Provider | No necesario | Requerido |
| Código repetitivo | Mínimo | Más verboso |
| Curva de aprendizaje | Baja | Mayor |
| DevTools | Vía middleware | Integrado |
💡 Mejores Prácticas
- • Usa funciones selectoras para prevenir re-renders innecesarios
- • Divide stores por dominio/característica para apps grandes
- • Usa persist middleware para datos que deben sobrevivir refrescos
- • Habilita devtools en desarrollo para depuración
- • Usa immer middleware para actualizaciones de estado anidado complejas