Retour au blog

Zustand : Le State Management Minimaliste qui Remplace Redux en 2025

Salut HaWkers, êtes-vous fatigué d'écrire des tonnes de boilerplate pour gérer l'état dans React avec Redux ? Ou confus par la complexité de la Context API pour les états globaux ?

En 2025, Zustand a émergé comme la solution minimaliste que les développeurs React attendaient. Avec seulement ~1KB de taille et une API incroyablement simple, Zustand remplace rapidement Redux dans les nouveaux projets et les migrations.

La bibliothèque a grandi de 300% en adoption l'année dernière, et de grandes entreprises ont déjà migré de Redux vers Zustand, réduisant le code jusqu'à 70% et améliorant la performance.

Le Problème avec Redux et les Alternatives

Redux : Puissant mais Verbeux

// Redux - beaucoup de boilerplate pour quelque chose de 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. Utilisation dans le composant
function Counter() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

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

// ~50 lignes pour un compteur ! 😱

Context API : Simple mais avec des Problèmes

// Context API - plus simple, mais re-renders inutiles
const CountContext = createContext();

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

  // ❌ Problème : TOUS les composants qui utilisent le contexte re-renderent
  // même s'ils n'utilisent qu'une partie de l'état
  return (
    <CountContext.Provider value={{ count, setCount }}>
      {children}
    </CountContext.Provider>
  );
}

function Counter() {
  const { count, setCount } = useContext(CountContext);
  // Re-render même si une autre valeur du contexte change
  return <div>{count}</div>;
}

Zustand : Minimalisme et Performance

Le même compteur en Zustand :

import { create } from 'zustand';

// Définit le store - C'EST TOUT !
const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 })
}));

// Utilise dans n'importe quel composant - sans Provider !
function Counter() {
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);

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

// ~15 lignes, performance optimisée automatiquement ! ✨

Avantages :

  • Zéro boilerplate : Pas d'actions, reducers, providers
  • Bundle minuscule : 1KB (Redux: ~10KB)
  • Sans Provider : Fonctionne hors de React aussi
  • Performance : Re-renders optimisés par défaut
  • TypeScript : Support natif excellent
  • DevTools : Intégration avec Redux DevTools

Cas d'Usage Avancés

1. Store d'Authentification Complet

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 dans localStorage
      partialize: (state) => ({  // Persiste uniquement les champs nécessaires
        user: state.user,
        token: state.token
      })
    }
  )
);

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

  if (!user) return <Login />;

  return (
    <div>
      <h1>Bonjour, {user.name}</h1>
      <button onClick={logout}>Déconnexion</button>
    </div>
  );
}

2. Panier d'Achat avec Calculs Dérivés

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

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

interface CartStore {
  items: CartItem[];
  // Valeurs calculées
  totalItems: () => number;
  totalPrice: () => number;
  // Actions
  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: [],

    // Valeurs calculées comme fonctions
    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: [] })
  }))
);

// Utilisation - re-renders optimisés
function Cart() {
  // Re-render uniquement si items change
  const items = useCartStore((state) => state.items);
  const totalPrice = useCartStore((state) => state.totalPrice());
  const removeItem = useCartStore((state) => state.removeItem);

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

function AddToCartButton({ product }) {
  // Ce composant NE re-render PAS quand le panier change !
  const addItem = useCartStore((state) => state.addItem);

  return <button onClick={() => addItem(product)}>Ajouter au Panier</button>;
}

Middleware et Extensions

1. Persist - LocalStorage Automatique

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

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

      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
      toggleNotifications: () =>
        set((state) => ({ notifications: !state.notifications }))
    }),
    {
      name: 'app-settings',
      storage: createJSONStorage(() => localStorage),
      // Migration de versions anciennes
      version: 1,
      migrate: (persistedState, version) => {
        if (version === 0) {
          // Migre de v0 à v1
          return { ...persistedState, notifications: true };
        }
        return persistedState;
      }
    }
  )
);

2. Immer - Immutabilité Simplifiée

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

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

    addTodo: (text) =>
      set((state) => {
        // Avec immer, vous pouvez "muter" directement !
        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; // Mutation directe !
        }
      }),

    removeTodo: (id) =>
      set((state) => {
        const index = state.todos.findIndex((t) => t.id === id);
        if (index !== -1) {
          state.todos.splice(index, 1); // Mutation directe !
        }
      })
  }))
);

3. Subscriptions - React Au-Delà de React

// Zustand fonctionne hors des composants React !
import { useUserStore } from './stores/user';

// S'abonne aux changements
const unsubscribe = useUserStore.subscribe(
  (state) => state.user,
  (user, previousUser) => {
    console.log('Utilisateur changé:', { user, previousUser });

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

// Accède à l'état hors d'un composant
const currentUser = useUserStore.getState().user;
console.log(currentUser);

// Met à jour l'état hors d'un composant
useUserStore.getState().login('user@example.com', 'password');

// Cleanup
unsubscribe();

Zustand vs. Autres Solutions

Comparaison de Taille de Bundle

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

// Zustand + Persist + DevTools: ~3KB (toujours plus petit que les alternatives !)

Comparaison de Performance

// Benchmark: 10 000 updates dans une liste de 1000 items

// Redux: ~850ms
// - Re-renders inutiles
// - Normalisation manuelle nécessaire

// Context API: ~1200ms
// - Tous les consommateurs re-renderent

// Zustand: ~420ms ✅
// - Re-renders optimisés par sélecteur
// - Zéro overhead

Comparaison d'Expérience Développeur

// Redux Toolkit (meilleure 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;

// Nécessite encore store, provider, etc...
// ~30 lignes de code

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

// ~6 lignes, même résultat ✅

Quand NE PAS Utiliser Zustand

✅ Utilisez Zustand quand :

  • Besoin d'état global simple
  • Voulez une performance optimisée
  • Préférez du code minimal
  • Travaillez sur un projet React moderne

⚠️ Considérez des alternatives quand :

  • L'équipe maîtrise déjà Redux et le projet est grand (coût de migration)
  • Besoin de time-travel debugging complexe
  • Application legacy avec fort couplage à Redux

Si vous voulez maîtriser React et le state management moderne, je vous recommande l'article Vue Vapor Mode : La Révolution qui Élimine le Virtual DOM où nous explorons les optimisations de performance dans les frameworks modernes.

C'est parti ! 🦅

📚 Maîtrisez React et JavaScript Moderne

Zustand représente l'avenir minimaliste du state management, mais maîtriser React et JavaScript est essentiel pour exploiter les outils modernes au maximum.

Options d'investissement :

  • €9,90 (paiement unique)

👉 Découvrir le Guide JavaScript

💡 Matériel mis à jour avec les meilleures pratiques du marché

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires