React LogoArchitecture front-end
Architecture/State management

Zustand avancé

Middleware

Dans une application e-commerce, si l'utilisateur rafraîchit la page, son panier ne doit pas disparaître. Avec le Context API ou Redux, synchroniser l'état avec le localStorage demande d'écrire des useEffect manuels ou de configurer des librairies tierces complexes.

Zustand intègre nativement un middleware persist qui fait tout le travail pour vous.

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface CartState {
  items: CartItem[];
  addItem: (item: CartItem) => void;
}

/* On enveloppe la configuration de notre store avec persist() */
export const useCartStore = create<CartState>()(
  persist(
    (set) => ({
      items: [],
      addItem: (item) => set((state) => ({ items: [...state.items, item] })),
    }),
    {
      name: 'cart-storage', // Le nom de la clé dans le localStorage
      // optionnel: partialize: (state) => ({ items: state.items }) // Si on ne veut sauvegarder qu'une partie du state
    }
  )
);

Note sur TypeScript : Remarquez les doubles parenthèses create<CartState>()(...). C'est une syntaxe obligatoire quand on utilise les middlewares de Zustand pour que l'inférence des types fonctionne correctement.

Derived State

Une erreur classique est de stocker des données qui pourraient être calculées à partir d'autres données. Par exemple, il ne faut jamais stocker le totalPrice ou le totalItems dans le store. Si vous le faites, vous devez penser à les mettre à jour à chaque fois qu'un article est ajouté ou supprimé.

La bonne pratique est de calculer ces états directement dans le composant au moment de la sélection.

import { useCartStore } from '@/store/useCartStore';

export const CartTotal = () => {
  /* On calcule le total à la volée. 
     Le composant se mettra à jour automatiquement si 'items' change. */
  const total = useCartStore((state) => 
    state.items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
  );

  return <div>Total : {total} €</div>;
};

Jotai et Recoil

Zustand n'est pas le seul acteur sur le marché. Vous entendrez souvent parler de Jotai ou Recoil.

Pourquoi avoir choisi Zustand pour ce cours ? Parce que Zustand utilise un modèle d'état global (un seul gros store), idéal pour les données métier (utilisateurs, panier, paramètres).

Jotai et Recoil utilisent un modèle atomique. Le store est fragmenté en centaines de petits "atomes" indépendants.

  • Quand utiliser Zustand ? 90% des applications (E-commerce, Dashboards, SaaS classiques).
  • Quand utiliser Jotai/Recoil ? Les applications avec énormément d'éléments UI très interdépendants et dynamiques (ex: Un clone de Figma, un éditeur de graphes, un jeu vidéo web).

Exercice

Dans cet exercice, vous allez finaliser le système de panier de notre application en implémentant les fonctionnalités avancées de Zustand.

Dans cet exercice, vous devrez :

  • ouvrir le fichier src/store/useCartStore.ts
  • importer et appliquer le middleware persist pour que le panier survive à un rafraîchissement de page (F5)
  • configurer le persist pour qu'il sauvegarde sous la clé "ecommerce-cart"
  • exclure la propriété isCartOpen de la sauvegarde (le panneau latéral doit toujours être fermé au chargement initial de l'application) en utilisant l'option partialize
  • créer un composant CartSummary.tsx qui affiche le nombre total d'articles et le prix total calculés à la volée (Derived State)