Manual Completo de Zustand Gestión de Estado Minimalista

Guía exhaustiva para dominar Zustand, la solución ligera y flexible para manejo de estado en React.

Fundamentos
Intermedio
Avanzado
Integraciones

1. Fundamentos de Zustand

1.1 ¿Qué es Zustand?

Zustand es una biblioteca de gestión de estado para React que ofrece:

1.2 Instalación

Terminal
npm install zustand

1.3 Creación de un Store Básico

src/store/counterStore.js
import { create } from 'zustand';

const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

export default useCounterStore;

1.4 Uso en Componentes

src/components/Counter.jsx
import useCounterStore from '../store/counterStore';

function Counter() {
  const { count, increment, decrement, reset } = useCounterStore();
  
  return (
    <div>
      <h1>Contador: {count}</h1>
      <button onClick={increment}>Incrementar</button>
      <button onClick={decrement}>Decrementar</button>
      <button onClick={reset}>Resetear</button>
    </div>
  );
}

export default Counter;

1.5 Selectores para Rendimiento

Evita rerenders innecesarios seleccionando solo el estado necesario:

src/components/CountDisplay.jsx
import useCounterStore from '../store/counterStore';

function CountDisplay() {
  const count = useCounterStore((state) => state.count);
  
  return <h1>Contador: {count}</h1>;
}

export default CountDisplay;

💡 Buenas Prácticas

2. Zustand Intermedio

2.1 Estado Complejo con Objetos Anidados

src/store/userStore.js
import { create } from 'zustand';

const useUserStore = create((set) => ({
  user: {
    id: null,
    name: '',
    email: '',
    preferences: {
      theme: 'light',
      notifications: true,
    },
  },
  setUser: (userData) => set({ user: { ...userData } }),
  updatePreferences: (prefs) => 
    set((state) => ({ 
      user: { 
        ...state.user, 
        preferences: { ...state.user.preferences, ...prefs } 
      } 
    })),
  clearUser: () => set({ 
    user: { id: null, name: '', email: '', preferences: {} } 
  }),
}));

export default useUserStore;

2.2 Acciones Asíncronas

src/store/authStore.js
import { create } from 'zustand';

const useAuthStore = create((set) => ({
  user: null,
  loading: false,
  error: null,
  
  login: async (credentials) => {
    set({ loading: true, error: null });
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
      });
      const user = await response.json();
      set({ user, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
  
  logout: () => {
    set({ user: null });
  },
}));

export default useAuthStore;

2.3 Middlewares

Zustand incluye middlewares útiles:

Persistencia en localStorage

Terminal
npm install zustand/middleware
src/store/themeStore.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useThemeStore = create(
  persist(
    (set) => ({
      theme: 'light',
      toggleTheme: () => 
        set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
    }),
    {
      name: 'theme-storage', // clave para localStorage
    }
  )
);

export default useThemeStore;

Middleware para Logging

src/store/withLogger.js
const withLogger = (config) => (set, get, api) => 
  config(
    (...args) => {
      console.log('  applying', args);
      set(...args);
      console.log('  new state', get());
    },
    get,
    api
  );

export default withLogger;
src/store/counterStore.js
import { create } from 'zustand';
import withLogger from './withLogger';

const useCounterStore = create(
  withLogger((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))
);

export default useCounterStore;

3. Zustand Avanzado

3.1 Stores Derivados (Computed Properties)

src/store/todoStore.js
import { create } from 'zustand';

const useTodoStore = create((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
      ),
    })),
}));

// Store derivado con getters
export const useTodoStats = create((get) => ({
  total: () => get(useTodoStore).todos.length,
  completed: () => get(useTodoStore).todos.filter(todo => todo.completed).length,
  pending: () => get(useTodoStore).todos.filter(todo => !todo.completed).length,
}));

3.2 Patrón de Slices (Divide y Vencerás)

src/store/useBoundStore.js
import { create } from 'zustand';

// Slice para autenticación
const createAuthSlice = (set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
});

// Slice para preferencias
const createPreferencesSlice = (set) => ({
  theme: 'light',
  language: 'es',
  setTheme: (theme) => set({ theme }),
  setLanguage: (language) => set({ language }),
});

// Combinar slices en un store
const useBoundStore = create((...a) => ({
  ...createAuthSlice(...a),
  ...createPreferencesSlice(...a),
}));

export default useBoundStore;

3.3 Suscripciones a Cambios Específicos

src/utils/subscribeToAuthChanges.js
import useAuthStore from '../store/authStore';

const unsubscribe = useAuthStore.subscribe(
  (state) => state.user,
  (user, prevUser) => {
    if (user !== prevUser) {
      console.log('Usuario cambió:', user);
      // Lógica adicional cuando el usuario cambia
    }
  }
);

// Para dejar de escuchar cambios
// unsubscribe();

3.4 Testing con Zustand

src/store/__tests__/counterStore.test.js
import { act } from 'react-test-renderer';
import useCounterStore from '../counterStore';

test('increment increases count by 1', () => {
  act(() => {
    useCounterStore.getState().increment();
  });
  
  expect(useCounterStore.getState().count).toBe(1);
});

test('reset sets count to 0', () => {
  // Estado inicial
  act(() => {
    useCounterStore.getState().increment();
    useCounterStore.getState().increment();
  });
  
  // Test reset
  act(() => {
    useCounterStore.getState().reset();
  });
  
  expect(useCounterStore.getState().count).toBe(0);
});

4. Integraciones con Zustand

4.1 Zustand + React Query

src/store/postsStore.js
import { create } from 'zustand';
import { useQuery } from 'react-query';

const fetchPosts = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  return await res.json();
};

const usePostsStore = create((set) => ({
  posts: [],
  setPosts: (posts) => set({ posts }),
}));

// Hook personalizado que combina ambos
export const usePosts = () => {
  const { posts, setPosts } = usePostsStore();
  const query = useQuery('posts', fetchPosts, {
    onSuccess: (data) => setPosts(data),
  });
  
  return { ...query, posts };
};

4.2 Zustand + Immer (Para estado inmutable)

Terminal
npm install immer
src/store/complexStore.js
import { create } from 'zustand';
import { produce } from 'immer';

const useComplexStore = create((set) => ({
  data: {
    nested: {
      items: [],
      config: {
        enabled: true,
        maxItems: 10,
      },
    },
  },
  
  addItem: (item) => 
    set(produce((state) => {
      state.data.nested.items.push(item);
    })),
    
  toggleEnabled: () => 
    set(produce((state) => {
      state.data.nested.config.enabled = !state.data.nested.config.enabled;
    })),
}));

export default useComplexStore;

4.3 Zustand + TypeScript

src/store/typedStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  loading: boolean;
  error: string | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      loading: false,
      error: null,
      login: async (email, password) => {
        set({ loading: true, error: null });
        try {
          // Lógica de login...
          set({ user: { id: '1', name: 'User', email }, loading: false });
        } catch (error) {
          set({ error: error.message, loading: false });
        }
      },
      logout: () => set({ user: null }),
    }),
    {
      name: 'auth-storage',
    }
  )
);

export default useAuthStore;

4.4 Zustand en Next.js

Para evitar hidratación inconsistente en SSR:

src/store/hydration.ts
import { useEffect, useState } from 'react';
import { useStore } from 'zustand';

export const useHydration = () => {
  const [hydrated, setHydrated] = useState(false);
  useEffect(() => {
    setHydrated(true);
  }, []);
  return hydrated;
};

export function useHydratedStore<T>(store: any, selector: (state: any) => T) {
  const state = useStore(store, selector);
  const hydrated = useHydration();
  return hydrated ? state : selector(store.getState());
}
src/components/Counter.tsx
import useCounterStore from '../store/counterStore';
import { useHydratedStore } from '../store/hydration';

function Counter() {
  const count = useHydratedStore(useCounterStore, (state) => state.count);
  const increment = useCounterStore((state) => state.increment);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}