Volver al blog

Zustand: El State Management Minimalista que Está Reemplazando a Redux en 2025

Hola HaWkers, ¿estás cansado de escribir toneladas de boilerplate para gestionar estado en React con Redux? ¿O confundido con la complejidad de Context API para estados globales?

En 2025, Zustand emergió como la solución minimalista que los desarrolladores React estaban esperando. Con solo ~1KB de tamaño y una API increíblemente simple, Zustand está rápidamente reemplazando a Redux en nuevos proyectos y migraciones.

La biblioteca creció un 300% en adopción el último año, y grandes empresas ya migraron de Redux a Zustand, reduciendo código hasta un 70% y mejorando el rendimiento.

El Problema con Redux y Alternativas

Redux: Poderoso pero Verboso

// Redux - mucho boilerplate para algo simple
// 1. Actions
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// 2. Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });

// 3. Reducer
const counterReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    default:
      return state;
  }
};

// 4. Store
const store = createStore(counterReducer);

// 5. Provider
<Provider store={store}>
  <App />
</Provider>

// 6. Uso en el componente
function Counter() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      {count}
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
}

// ¡~50 líneas para un contador! 😱

Context API: Simple pero con Problemas

// Context API - más simple, pero re-renders innecesarios
const CountContext = createContext();

function CountProvider({ children }) {
  const [count, setCount] = useState(0);

  // ❌ Problema: TODO componente que usa el contexto se re-renderiza
  // incluso si usa solo parte del estado
  return (
    <CountContext.Provider value={{ count, setCount }}>
      {children}
    </CountContext.Provider>
  );
}

function Counter() {
  const { count, setCount } = useContext(CountContext);
  // Se re-renderiza incluso si otro valor en el contexto cambia
  return <div>{count}</div>;
}

Zustand: Minimalismo y Rendimiento

El mismo contador en Zustand:

import { create } from 'zustand';

// Define store - ¡SOLO ESTO!
const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 })
}));

// Usa en cualquier componente - ¡sin Provider!
function Counter() {
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);

  return (
    <div>
      {count}
      <button onClick={increment}>+</button>
    </div>
  );
}

// ¡~15 líneas, rendimiento optimizado automáticamente! ✨

Ventajas:

  • Zero boilerplate: Sin actions, reducers, providers
  • Tiny bundle: 1KB (Redux: ~10KB)
  • Sin Provider: Funciona fuera de React también
  • Rendimiento: Re-renders optimizados por defecto
  • TypeScript: Soporte nativo excelente
  • DevTools: Integración con Redux DevTools

Casos de Uso Avanzados

1. Store de Autenticación Completa

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

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

interface AuthStore {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  updateUser: (user: Partial<User>) => void;
}

export const useAuthStore = create<AuthStore>()(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: async (email, password) => {
        try {
          const response = await fetch('/api/login', {
            method: 'POST',
            body: JSON.stringify({ email, password })
          });

          const { user, token } = await response.json();

          set({
            user,
            token,
            isAuthenticated: true
          });
        } catch (error) {
          console.error('Login failed:', error);
          throw error;
        }
      },

      logout: () => {
        set({
          user: null,
          token: null,
          isAuthenticated: false
        });
      },

      updateUser: (updates) => {
        set((state) => ({
          user: state.user ? { ...state.user, ...updates } : null
        }));
      }
    }),
    {
      name: 'auth-storage',  // Persiste en localStorage
      partialize: (state) => ({  // Persiste solo campos necesarios
        user: state.user,
        token: state.token
      })
    }
  )
);

// Uso
function Profile() {
  const user = useAuthStore((state) => state.user);
  const logout = useAuthStore((state) => state.logout);

  if (!user) return <Login />;

  return (
    <div>
      <h1>Hola, {user.name}</h1>
      <button onClick={logout}>Salir</button>
    </div>
  );
}

2. Shopping Cart con Cálculos Derivados

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartStore {
  items: CartItem[];
  // Valores computados
  totalItems: () => number;
  totalPrice: () => number;
  // Acciones
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
}

export const useCartStore = create<CartStore>()(
  devtools((set, get) => ({
    items: [],

    // Valores computados como funciones
    totalItems: () => {
      return get().items.reduce((sum, item) => sum + item.quantity, 0);
    },

    totalPrice: () => {
      return get().items.reduce(
        (sum, item) => sum + item.price * item.quantity,
        0
      );
    },

    addItem: (newItem) => {
      set((state) => {
        const existingItem = state.items.find((item) => item.id === newItem.id);

        if (existingItem) {
          return {
            items: state.items.map((item) =>
              item.id === newItem.id
                ? { ...item, quantity: item.quantity + 1 }
                : item
            )
          };
        }

        return {
          items: [...state.items, { ...newItem, quantity: 1 }]
        };
      });
    },

    removeItem: (id) => {
      set((state) => ({
        items: state.items.filter((item) => item.id !== id)
      }));
    },

    updateQuantity: (id, quantity) => {
      if (quantity <= 0) {
        get().removeItem(id);
        return;
      }

      set((state) => ({
        items: state.items.map((item) =>
          item.id === id ? { ...item, quantity } : item
        )
      }));
    },

    clearCart: () => set({ items: [] })
  }))
);

// Uso - re-renders optimizados
function Cart() {
  // Solo se re-renderiza si items cambia
  const items = useCartStore((state) => state.items);
  const totalPrice = useCartStore((state) => state.totalPrice());
  const removeItem = useCartStore((state) => state.removeItem);

  return (
    <div>
      <h2>Carrito</h2>
      {items.map((item) => (
        <div key={item.id}>
          {item.name} - {item.quantity}x ${item.price}
          <button onClick={() => removeItem(item.id)}>Eliminar</button>
        </div>
      ))}
      <p>Total: ${totalPrice.toFixed(2)}</p>
    </div>
  );
}

function AddToCartButton({ product }) {
  // ¡Este componente NO se re-renderiza cuando el carrito cambia!
  const addItem = useCartStore((state) => state.addItem);

  return <button onClick={() => addItem(product)}>Agregar al Carrito</button>;
}

Middleware y Extensiones

1. Persist - LocalStorage Automático

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

const useSettingsStore = create(
  persist(
    (set) => ({
      theme: 'light',
      language: 'es-ES',
      notifications: true,

      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
      toggleNotifications: () =>
        set((state) => ({ notifications: !state.notifications }))
    }),
    {
      name: 'app-settings',
      storage: createJSONStorage(() => localStorage),
      // Migración de versiones antiguas
      version: 1,
      migrate: (persistedState, version) => {
        if (version === 0) {
          // Migra de v0 a v1
          return { ...persistedState, notifications: true };
        }
        return persistedState;
      }
    }
  )
);

2. Immer - Inmutabilidad Simplificada

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useTodoStore = create(
  immer((set) => ({
    todos: [],

    addTodo: (text) =>
      set((state) => {
        // ¡Con immer, puedes "mutar" directamente!
        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; // ¡Mutación directa!
        }
      }),

    removeTodo: (id) =>
      set((state) => {
        const index = state.todos.findIndex((t) => t.id === id);
        if (index !== -1) {
          state.todos.splice(index, 1); // ¡Mutación directa!
        }
      })
  }))
);

3. Subscriptions - React Más Allá de React

// ¡Zustand funciona fuera de componentes React!
import { useUserStore } from './stores/user';

// Subscribe a cambios
const unsubscribe = useUserStore.subscribe(
  (state) => state.user,
  (user, previousUser) => {
    console.log('Usuario cambió:', { user, previousUser });

    // Analytics tracking
    if (user && !previousUser) {
      analytics.track('User Logged In', { userId: user.id });
    }
  }
);

// Accede al estado fuera de componente
const currentUser = useUserStore.getState().user;
console.log(currentUser);

// Actualiza estado fuera de componente
useUserStore.getState().login('user@example.com', 'password');

// Cleanup
unsubscribe();

Zustand vs. Otras Soluciones

Comparación de Bundle Size

Redux + Redux Toolkit:  ~10KB
MobX:                   ~16KB
Recoil:                 ~14KB
Jotai:                  ~3KB
Zustand:                ~1KB  ✅

// Zustand + Persist + DevTools: ~3KB (¡aún menor que las alternativas!)

Comparación de Rendimiento

// Benchmark: 10,000 updates en lista de 1000 ítems

// Redux: ~850ms
// - Re-renders innecesarios
// - Normalización manual necesaria

// Context API: ~1200ms
// - Todos los consumidores se re-renderizan

// Zustand: ~420ms ✅
// - Re-renders optimizados por selector
// - Zero overhead

Comparación de Developer Experience

// Redux Toolkit (mejor DX de Redux)
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1 },
    decrement: (state) => { state.value -= 1 }
  }
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

// Todavía necesita store, provider, etc...
// ~30 líneas de código

// Zustand
const useCounter = create((set) => ({
  value: 0,
  increment: () => set((s) => ({ value: s.value + 1 })),
  decrement: () => set((s) => ({ value: s.value - 1 }))
}));

// ~6 líneas, mismo resultado ✅

Cuándo NO Usar Zustand

✅ Usa Zustand cuando:

  • Necesitas state global simple
  • Quieres rendimiento optimizado
  • Prefieres código mínimo
  • Trabajas en proyecto React moderno

⚠️ Considera alternativas cuando:

  • El equipo ya domina Redux y el proyecto es grande (costo de migración)
  • Necesitas time-travel debugging complejo
  • Aplicación legada con fuerte acoplamiento a Redux

Si quieres dominar React y state management moderno, te recomiendo el artículo Vue Vapor Mode: La Revolución que Elimina el Virtual DOM donde exploramos optimizaciones de rendimiento en frameworks modernos.

¡Vamos a por ello! 🦅

📚 Domina React y JavaScript Moderno

Zustand representa el futuro minimalista del state management, pero dominar React y JavaScript es esencial para aprovechar herramientas modernas al máximo.

Opciones de inversión:

  • $9.90 USD (pago único)

👉 Conocer la Guía JavaScript

💡 Material actualizado con las mejores prácticas del mercado

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios