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 :
- Zéro Provider : Fini le "Provider Hell" dans votre
App.tsx. Le store est un hook indépendant. - Minimaliste : Les actions et l'état sont définis au même endroit, sans
switchni types d'actions complexes. - 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/storeet y ajouter un fichieruseCartStore.ts - typer correctement l'interface
CartState(n'oubliez pas d'inclure les fonctionsaddItem,removeItem, ettoggleCart) - 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/contextqui 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.