TechLead

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