W
Web Dev Simplified
#Zustand#React#Gestion d'état

Zustand : Guide Complet de Gestion d'État pour React

Découvrez Zustand, la bibliothèque de gestion d'état légère et performante pour React. Apprenez à l'utiliser, à optimiser les rendus et à gérer le state persistant avec Immer et Persist.

5 min de lectureGuide IA

Introduction

Zustand est une bibliothèque de gestion d'état légère et rapide, idéale pour construire des applications React, des compteurs simples aux jeux de cartes complexes directement dans le navigateur.

Précis de configuration

Élément Version / Lien
Langage / Runtime JavaScript / TypeScript, Node.js
Librairie principale Zustand
APIs requises create (de 'zustand'), shallow (de 'zustand/shallow'), persist (de 'zustand/middleware'), immer (de 'zustand/middleware/immer')
Clés / credentials nécessaires Aucune

Guide étape par étape

Étape 1 — Installation de Zustand

Pour commencer à utiliser Zustand dans votre projet, vous devez l'installer via npm ou yarn. Cette commande ajoute la bibliothèque à vos dépendances.

npm install zustand

Étape 2 — Création d'un Store Zustand de base

Un store Zustand est créé à l'aide de la fonction create. Il s'agit d'un hook personnalisé qui encapsule votre état et vos actions. Pour une meilleure typisation, il est recommandé de définir une interface pour votre état.

// src/state/useCounterStore.ts
import { create } from "zustand";

type CounterState = {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
};

export const useCounterStore = create<CounterState>((set) => ({
  count: 0, // État initial du compteur
  increment: () => set((state) => ({ count: state.count + 1 })), // Action pour incrémenter
  decrement: () => set((state) => ({ count: state.count - 1 })), // Action pour décrémenter
  reset: () => set(() => ({ count: 0 })), // Action pour réinitialiser
}));

Étape 3 — Consommation du Store dans les Composants React

Pour utiliser l'état et les actions de votre store Zustand dans un composant React, vous appelez simplement le hook useCounterStore et sélectionnez les parties de l'état dont vous avez besoin. Zustand gère automatiquement les re-rendus de manière optimisée.

// src/components/Counter.tsx
import { useCounterStore } from "../state/useCounterStore";
import { shallow } from 'zustand/shallow'; // Importez shallow pour l'optimisation

// ... (autres imports et hooks comme useFlash si nécessaire)

export function Counter() {
  return (
    <>
      <CountDisplay />
      <CountControls />
    </>
  );
}

function CountDisplay() {
  // Sélectionne uniquement 'count' pour que ce composant ne se rende que si 'count' change
  const count = useCounterStore((state) => state.count);
  // ... (autres hooks et rendu du display)
  return (
    <div className="card">
      <p className="count-display">{count}</p>
      <p className="render-count">Renders: {renders}</p>
    </div>
  );
}

function CountControls() {
  // Sélectionne les actions et utilise 'shallow' pour éviter les re-rendus inutiles
  const { increment, decrement, reset } = useCounterStore(
    (state) => ({
      increment: state.increment,
      decrement: state.decrement,
      reset: state.reset,
    }),
    shallow // Utilise shallow pour une comparaison superficielle de l'objet retourné
  );
  // ... (autres hooks et rendu des contrôles)
  return (
    <div className="card">
      <div className="button-group">
        <button onClick={decrement}>-1</button>
        <button onClick={reset}>Reset</button>
        <button onClick={increment}>+1</button>
        <button onClick={incrementOutsideReact}>+1 Outside</button>
      </div>
      <p className="render-count">Renders: {renders}</p>
    </div>
  );
}

// Fonction pour incrémenter le compteur en dehors d'un composant React
function incrementOutsideReact() {
  console.log("Outside of React");
  useCounterStore.getState().increment(); // Accès direct à l'état et aux actions du store
}

Étape 4 — Accès au Store en dehors des Composants React

Zustand permet d'accéder à l'état et aux actions du store directement, sans être dans un composant React. C'est utile pour les logiques métier ou les interactions avec des APIs externes.

// Exemple d'utilisation en dehors d'un composant React
// Pour obtenir l'état actuel :
const currentCount = useCounterStore.getState().count;
console.log("Current count outside React:", currentCount);

// Pour mettre à jour l'état :
useCounterStore.setState({ count: 10 }); // Définit le compteur à 10

// Pour appeler une action :
useCounterStore.getState().increment(); // Incrémente le compteur

Étape 5 — Structuration des Actions dans un sous-objet

Pour une meilleure organisation, il est courant de regrouper les actions dans un sous-objet actions au sein de votre CounterState. Cela clarifie la distinction entre l'état et les fonctions qui le modifient.

// src/state/useCounterStore.ts
// ... (imports)

type CounterActions = {
  increment: () => void;
  decrement: () => void;
  reset: () => void;
};

type CounterState = {
  count: number;
  actions: CounterActions; // Les actions sont maintenant dans un sous-objet
};

export const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  actions: { // Définition des actions dans le sous-objet
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 })),
    reset: () => set(() => ({ count: 0 })),
  },
}));

// Dans les composants, vous accédez aux actions via state.actions
// Exemple pour CountControls :
// const { actions } = useCounterStore((state) => ({ actions: state.actions }), shallow);
// <button onClick={actions.increment}>+1</button>

// Exemple pour incrementOutsideReact :
// useCounterStore.getState().actions.increment();

Étape 6 — Persistance de l'état avec persist

Persistance de l'état avec persist
Le middleware persist de Zustand permet de sauvegarder automatiquement l'état de votre store dans le stockage local (ou autre) et de le recharger lors de l'initialisation. Cela assure que l'état de l'utilisateur est conservé entre les sessions.

// src/state/useCounterStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware"; // Importez le middleware persist

// ... (types CounterActions et CounterState)

export const useCounterStore = create<CounterState>(
  persist(
    (set) => ({
      count: 0,
      actions: {
        increment: () => set((state) => ({ count: state.count + 1 })),
        decrement: () => set((state) => ({ count: state.count - 1 })),
        reset: () => set(() => ({ count: 0 })),
      },
    }),
    { name: "count" } // Options de persistance : 'name' est la clé dans le stockage local
  )
);

Étape 7 — Gestion de l'état imbriqué avec immer

Gestion de l'état imbriqué avec immer
Lorsque votre état devient complexe et imbriqué, la mise à jour immuable peut devenir fastidieuse. Le middleware immer permet de muter directement un brouillon de l'état, et Immer s'occupe de produire un nouvel état immuable en arrière-plan, simplifiant grandement le code.

// src/state/useCounterStore.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer"; // Importez le middleware immer

// ... (types CounterActions et CounterState)

type UserAddress = {
  street: string;
  zipcode: string;
};

type User = {
  name: string;
  address: UserAddress;
};

type CounterState = {
  count: number;
  user: User; // Ajout d'un état utilisateur imbriqué
  actions: CounterActions;
};

export const useCounterStore = create<CounterState>(
  immer(
    persist(
      (set) => ({
        count: 0,
        user: { // État initial de l'utilisateur
          name: "Kyle",
          address: {
            street: "Main St",
            zipcode: "23423",
          },
        },
        actions: {
          increment: () => set((state) => { state.count++; }), // Mutation directe grâce à Immer
          decrement: () => set((state) => { state.count--; }),
          reset: () => set((state) => { state.count = 0; }),
          updateStreet: (newStreet: string) => set((state) => { // Mise à jour imbriquée simplifiée
            state.user.address.street = newStreet;
          }),
        },
      }),
      { name: "count" } // Options de persistance
    )
  )
);

Tableaux comparatifs

Caractéristique React Context Zustand
Re-rendus Tous les composants consommateurs re-rendent sur tout changement de contexte. Seuls les composants sélectionnant la partie modifiée de l'état re-rendent.
Scalabilité Difficile à scaler pour les grandes applications avec des états globaux complexes. Très scalable, conçu pour les applications de toute taille.
API Basée sur useContext et useReducer (souvent). Basée sur un hook create simple, avec des middlewares optionnels.
Accès hors React Nécessite de passer les fonctions via props ou de recréer le contexte. Accès direct à l'état et aux actions via useStore.getState() et useStore.setState().
Boilerplate Peut être verbeux avec les Provider et Consumer. Minimaliste, moins de boilerplate.
Optimisation Nécessite React.memo ou useCallback pour optimiser les re-rendus. Optimisation des re-rendus intégrée via la sélection de l'état et shallow.

⚠️ Erreurs fréquentes et pièges

  1. Re-rendus excessifs avec la sélection d'objets : Si vous sélectionnez un objet directement (ex: const { actions } = useCounterStore((state) => ({ actions: state.actions }));), React recréera une nouvelle référence d'objet à chaque rendu, provoquant des re-rendus inutiles.
    Solution : Utilisez le comparateur shallow de Zustand : const { actions } = useCounterStore((state) => ({ actions: state.actions }), shallow);

  2. Mise à jour immuable manuelle de l'état imbriqué : Sans immer, la mise à jour d'objets profondément imbriqués nécessite de copier manuellement chaque niveau de l'objet, ce qui est source d'erreurs et de verbosité.
    Solution : Intégrez le middleware immer pour permettre des mutations directes et simplifiées de l'état.

  3. Oubli de la typisation avec TypeScript : Travailler avec Zustand et TypeScript sans définir correctement les types peut entraîner des erreurs de compilation et une perte des avantages de la typisation.
    Solution : Définissez toujours des interfaces claires (CounterState, CounterActions) et utilisez-les avec la fonction create<YourState>.

Glossaire

Store : Conteneur centralisé pour l'état global d'une application, accessible par tous les composants qui en ont besoin.
Middleware : Fonction qui s'intercale entre l'action et la mise à jour de l'état, permettant d'ajouter des fonctionnalités comme la persistance ou la journalisation.
Shallow Equality : Comparaison superficielle entre deux objets, vérifiant si leurs propriétés de premier niveau sont identiques en référence, sans inspecter les objets imbriqués.

Points clés à retenir

  • Zustand est une alternative légère et performante à React Context pour la gestion d'état.
  • Il permet une sélection granulaire de l'état, minimisant les re-rendus des composants.
  • Le hook useShallow est essentiel pour optimiser les sélections d'objets et éviter les re-rendus inutiles.
  • L'état et les actions du store peuvent être accédés en dehors des composants React, offrant une grande flexibilité.
  • Les middlewares comme persist et immer étendent les fonctionnalités du store pour la persistance et la gestion simplifiée de l'état immuable.
  • La typisation avec TypeScript est fortement recommandée pour maintenir la robustesse du code.