frontend / react
State management externe : Zustand et Redux
Explication
Ce que vous allez apprendre
- Comprendre pourquoi Context devient un obstacle de performance à grande échelle
- Créer un store Zustand et l'exploiter avec des hooks sélecteurs ciblés
- Découper un state Redux Toolkit en slices avec des reducers immuables en apparence
- Persister un store dans le localStorage avec le middleware
persist - Choisir entre
useState, Context et une bibliothèque externe selon l'échelle réelle du besoin
Dans quel contexte ?
Sur src/features/panier/, le composant CompteurPanier du header se re-rend à chaque changement du panier — y compris ceux qui ne concernent que la modification d'une quantité dans un article déjà présent — provoquant un clignotement visible à chaque interaction ailleurs dans l'application via PanierContext. En migrant vers un store Zustand avec un sélecteur précis (etat => etat.articles.length), seul ce composant se re-rend quand le nombre d'articles change réellement, plus jamais pour les autres mutations du panier.
Étape 1 : les limites de Context à grande échelle
Context et useReducer couvrent bien des besoins de partage d'état modéré. Mais à mesure qu'une application grandit, la limite structurelle de Context devient un vrai obstacle.
Étape 2 : la solution, un store externe
C'est ce vide que comblent les bibliothèques de state management externe, en introduisant un "store" : un espace de state global vivant en dehors de l'arbre de composants React.
Étape 3 : la différence essentielle avec Context
Au lieu de re-rendre tous les consommateurs à chaque changement, un store bien conçu ne re-rend QUE les composants qui utilisent précisément la portion de données qui vient de changer.
Étape 4 : la philosophie de Zustand
Zustand mise sur la simplicité : un store se crée en quelques lignes, sans Provider à poser dans l'arbre, chaque composant sélectionnant uniquement la tranche dont il a besoin.
Étape 5 : la philosophie de Redux Toolkit
Redux Toolkit, plus structuré, organise le state en "slices" et impose un flux de données plus strict, au prix d'un peu plus de code de mise en place.
Étape 6 : comment choisir entre les deux
Il n'existe pas de solution universellement meilleure : le choix dépend de l'échelle réelle du besoin.
Piège fréquent
Introduire Redux Toolkit ou Zustand dès le début d'un petit projet ajoute du code de mise en place (store, slices, sélecteurs) pour un bénéfice souvent nul tant que Context et useReducer suffisent largement. Attends un vrai symptôme de performance ou de complexité avant de migrer.
La règle pratique à retenir
Un state isolé reste mieux servi par useState local, un partage modéré se contente de Context, et une bibliothèque externe ne se justifie qu'à partir d'un vrai besoin de scalabilité.
| Besoin | Solution | Re-rendu ciblé ? |
|---|---|---|
| State isolé à un composant | useState | N/A |
| Partage modéré, peu de mises à jour | Context + useReducer | Non (tous les consommateurs) |
| State global, mises à jour fréquentes | Zustand | Oui, via sélecteurs |
| Grosse app, traçabilité/middleware poussés | Redux Toolkit | Oui, via useSelector |
Et ensuite ?
Une fois le state management compris, la prochaine étape prend du recul sur l'architecture globale d'une grosse application React.
Commandes & code
State management externe
Quand Context + useReducer ne suffit plus : state global performant, partagé entre de nombreux composants distants.
// Zustand : API minimale, pas de Provider requis, basé sur un store + des hooks sélecteurs
import { create } from "zustand";
const usePanierStore = create((set, get) => ({
articles: [],
ajouter: (article) => set((etat) => ({
articles: [...etat.articles, article],
})),
supprimer: (id) => set((etat) => ({
articles: etat.articles.filter((a) => a.id !== id),
})),
total: () => get().articles.reduce((s, a) => s + a.prix * a.quantite, 0),
}));
// Sélecteur : le composant ne re-rend QUE si LA VALEUR SÉLECTIONNÉE change, pas tout le store
function CompteurPanier() {
const nbArticles = usePanierStore((etat) => etat.articles.length);
return <span>{nbArticles} articles</span>;
}
function BoutonAjouter({ produit }) {
const ajouter = usePanierStore((etat) => etat.ajouter);
return <button onClick={() => ajouter(produit)}>Ajouter au panier</button>;
}
// aucun Provider à poser dans l'arbre : le store est importé directement où nécessaire// Redux Toolkit : plus structuré, utile pour de très grosses applications avec middleware/devtools poussés
import { createSlice, configureStore } from "@reduxjs/toolkit";
import { useSelector, useDispatch, Provider } from "react-redux";
const panierSlice = createSlice({
name: "panier",
initialState: { articles: [] },
reducers: {
// Redux Toolkit autorise une syntaxe "mutable" en apparence (via Immer en interne)
ajouter: (etat, action) => {
etat.articles.push(action.payload);
},
supprimer: (etat, action) => {
etat.articles = etat.articles.filter((a) => a.id !== action.payload);
},
},
});
export const { ajouter, supprimer } = panierSlice.actions;
const store = configureStore({
reducer: { panier: panierSlice.reducer },
});
function App() {
return (
<Provider store={store}>
<Panier />
</Provider>
);
}
function Panier() {
const articles = useSelector((etat) => etat.panier.articles);
const dispatch = useDispatch();
return (
<ul>
{articles.map((a) => (
<li key={a.id}>
{a.nom}
<button onClick={() => dispatch(supprimer(a.id))}>Retirer</button>
</li>
))}
</ul>
);
}// Persistance et middleware : cas fréquent avec Zustand
import { persist } from "zustand/middleware";
const usePreferencesStore = create(
persist(
(set) => ({
theme: "clair",
changerTheme: (theme) => set({ theme }),
}),
{ name: "preferences-storage" } // clé localStorage, synchronisée automatiquement
)
);| Solution | Quand la choisir |
|---|---|
useState local | State isolé à un composant |
Context + useReducer | Partage modéré, sans lib externe |
| Zustand | State global simple, API minimale, pas de boilerplate |
| Redux Toolkit | Grosse app, besoin de middleware/devtools/traçabilité poussés |
Résumé
- Zustand expose un store consommé via des hooks sélecteurs, sans
Providerobligatoire dans l'arbre. - Un sélecteur précis (
etat => etat.x) limite les re-rendus à ce qui a réellement changé. - Redux Toolkit structure fortement le state via des slices, avec Immer pour une écriture "mutable" en apparence.
- Le choix dépend de l'échelle : Context suffit souvent, une lib externe se justifie à partir d'un vrai besoin de scalabilité.
Exercices pratiques
Mission : traquer le clignotement persistant du CompteurPanier
Objectif : Diagnostiquer pourquoi un sélecteur Zustand mal choisi continue de provoquer des re-rendus inutiles, puis corriger le sélecteur.
Contexte
Après la migration de PanierContext vers un store Zustand, CompteurPanier utilise const panier = usePanierStore((etat) => etat.articles); puis affiche panier.length. Le clignotement a diminué mais n'a pas disparu : le compteur continue de se re-rendre quand un autre composant modifie uniquement la quantité d'un article déjà présent dans le panier, sans jamais ajouter ni retirer d'article.