Retour au cours

frontend / react

State management externe : Zustand et Redux

Leçon 201 exercice

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é.

BesoinSolutionRe-rendu ciblé ?
State isolé à un composantuseStateN/A
Partage modéré, peu de mises à jourContext + useReducerNon (tous les consommateurs)
State global, mises à jour fréquentesZustandOui, via sélecteurs
Grosse app, traçabilité/middleware poussésRedux ToolkitOui, 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.

jsx
// 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
jsx
// 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>
    );
}
jsx
// 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
    )
);
SolutionQuand la choisir
useState localState isolé à un composant
Context + useReducerPartage modéré, sans lib externe
ZustandState global simple, API minimale, pas de boilerplate
Redux ToolkitGrosse app, besoin de middleware/devtools/traçabilité poussés

Résumé

  • Zustand expose un store consommé via des hooks sélecteurs, sans Provider obligatoire 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

1 disponible
1

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.

Résoudre l’exercice →