React LogoArchitecture front-end
Architecture/State management

Zustand

Vous venez de voir les limites de l'approche native de React (Context + useReducer). Pour contourner ça, on a pendant un temps utilisé Redux.
Mais Redux souffre du même problème : pas mal de "boilerplate" (actions, reducers, dispatchers, providers) pour faire des choses simples.

Aujourd'hui, pour la gestion d'état global on utilise des outils comme Zustand ou Jotai.

Pourquoi Zustand ?

Zustand résout tous les problèmes du Context API et de Redux en même temps :

  1. Zéro Provider : Fini le "Provider Hell" dans votre App.tsx. Le store est un hook indépendant.
  2. Minimaliste : Les actions et l'état sont définis au même endroit, sans switch ni types d'actions complexes.
  3. Performant par défaut : Contrairement au Context API qui re-rend tous ses enfants, Zustand utilise un système de sélecteurs. Un composant ne se re-rendra que si la donnée précise qu'il écoute a changé.

Créer et utiliser un store Zustand

Avec TypeScript, la création d'un store se fait en deux étapes très simples : définir l'interface, puis implémenter le store. Prenons l'exemple d'un système de notifications global :

/* store/useNotificationStore.ts */
import { create } from 'zustand';

/* 1. On définit l'interface stricte de notre état ET de nos actions */
interface NotificationState {
  notifications: string[];
  areNotificationsMuted: boolean;
  addNotification: (message: string) => void;
  removeNotification: (index: number) => void;
  clearAll: () => void;
}

/* 2. On crée le store. 
   La fonction "set" permet de fusionner le nouvel état avec l'ancien. */
export const useNotificationStore = create<NotificationState>((set) => ({
  // État initial
  notifications: [],
  areNotificationsMuted: false,

  // Actions
  addNotification: (message) => set((state) => ({ 
    notifications: [...state.notifications, message] 
  })),
  
  removeNotification: (indexToRemove) => set((state) => ({ 
    notifications: state.notifications.filter((_, index) => index !== indexToRemove) 
  })),

  clearAll: () => set({ notifications: [] })
}));

C'est tout. Pas de Context.Provider, pas de useReducer, pas de dispatch({ type: '...' }).

L'importance des sélecteurs

Pour utiliser ce store dans un composant, on appelle le hook en lui passant une fonction fléchée : le sélecteur. C'est ce qui garantit les performances de votre application.

/* components/NotificationBadge.tsx */
import { useNotificationStore } from '@/store/useNotificationStore';

export const NotificationBadge = () => {
  /* BONNE PRATIQUE : On sélectionne UNIQUEMENT ce dont on a besoin.
     Si areNotificationsMuted change, ce composant NE SE RE-RENDRA PAS. */
  const notifications = useNotificationStore((state) => state.notifications);

  /* MAUVAISE PRATIQUE : Si vous faites ça, le composant se re-rendra 
     à CHAQUE modification du store (comme avec le Context API). */
  // const store = useNotificationStore(); 

  return <div>{notifications.length} messages non lus</div>;
};

Exercice

L'idée est de reprendre exactement la même application e-commerce que lors de l'exercice précédent.
Sauf que cette fois, vous migrez vers Zustand.

Votre objectif est de recréer toute la logique métier du panier, mais de manière beaucoup plus propre et scalable.

Dans cet exercice, vous devrez :

  • créer un dossier src/store et y ajouter un fichier useCartStore.ts
  • typer correctement l'interface CartState (n'oubliez pas d'inclure les fonctions addItem, removeItem, et toggleCart)
  • implémenter le store Zustand en utilisant la fonction set
  • aller dans les composants (Header.tsx, ProductCard.tsx, CartSidebar.tsx) pour remplacer les anciens appels au Context par votre nouveau hook Zustand
  • supprimer définitivement l'ancien dossier src/context qui ne sert plus à rien
  • Bonus : Séparez les composants d'affichage des données et les composants d'interaction et utilisez des sélecteurs stricts dans vos composants pour optimiser les rendus

Pensez à sauvegarder votre travail, on en aura besoin pour l'exercice suivant.