Zustand + Next.js App Router : Gestion d'État React Moderne du Zéro à la Production

Noqta TeamAI Bot
Par Noqta Team & AI Bot ·

Chargement du lecteur de synthèse vocale...

Une gestion d'état légère qui fonctionne, tout simplement. Zustand est le gestionnaire d'état minimaliste basé sur les hooks qui est devenu le choix privilégié des développeurs React en 2026. Dans ce tutoriel, vous allez construire une application de panier d'achat réaliste avec Next.js 15 App Router, en apprenant tous les patterns Zustand nécessaires pour la production.

Ce que vous allez apprendre

À la fin de ce tutoriel, vous serez capable de :

  • Configurer Zustand dans un projet Next.js 15 App Router avec TypeScript
  • Créer des stores type-safe avec des actions, des sélecteurs et des valeurs calculées
  • Utiliser les middleware pour le logging, la persistance et l'intégration devtools
  • Gérer le rendu côté serveur et l'hydratation avec les stores Zustand
  • Implémenter des store slices pour organiser un état complexe
  • Construire un état persistant avec localStorage et des moteurs de stockage personnalisés
  • Appliquer des patterns concrets pour un panier d'achat en production

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé (node --version)
  • Une expérience en TypeScript (types, génériques, hooks)
  • Une familiarité avec les bases de Next.js App Router (layouts, pages, composants serveur/client)
  • Une compréhension de base de l'état React (useState, useContext)

Pourquoi Zustand en 2026 ?

La gestion d'état React a considérablement évolué. Alors que Redux a dominé pendant des années, l'écosystème s'est orienté vers des solutions plus simples et plus ergonomiques. Voici pourquoi Zustand se démarque :

CaractéristiqueZustandRedux ToolkitJotaiContext API
Taille du bundle~1 Ko~12 Ko~3 Ko0 Ko (natif)
BoilerplateMinimalModéréMinimalÉlevé à grande échelle
DX TypeScriptExcellenteBonneBonneManuelle
Support SSRNatifConfig supplémentaireNatifNatif
DevtoolsVia middlewareIntégréExtensionAucun
Courbe d'apprentissageFaibleModéréeFaibleFaible (monte mal en charge)

Zustand vous offre la simplicité de useState avec la puissance d'un store global, sans aucun provider requis.


Étape 1 : Configuration du Projet

Créez un nouveau projet Next.js 15 avec TypeScript :

npx create-next-app@latest zustand-cart --typescript --tailwind --app --src-dir
cd zustand-cart

Installez Zustand :

npm install zustand

Votre structure de projet devrait ressembler à ceci :

zustand-cart/
├── src/
│   ├── app/
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── cart/
│   │       └── page.tsx
│   ├── components/
│   ├── stores/
│   └── types/
├── package.json
└── tsconfig.json

Créez les répertoires nécessaires :

mkdir -p src/stores src/types src/components

Étape 2 : Définir vos Types

Commencez par des définitions de types propres. Créez src/types/cart.ts :

// src/types/cart.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  image: string;
  category: string;
}
 
export interface CartItem {
  product: Product;
  quantity: number;
}
 
export interface CartSummary {
  totalItems: number;
  totalPrice: number;
  discount: number;
  finalPrice: number;
}

Étape 3 : Créer votre Premier Store Zustand

C'est là que Zustand brille. Créez src/stores/cart-store.ts :

// src/stores/cart-store.ts
import { create } from "zustand";
import type { Product, CartItem } from "@/types/cart";
 
interface CartState {
  // État
  items: CartItem[];
  isOpen: boolean;
 
  // Actions
  addItem: (product: Product) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  toggleCart: () => void;
 
  // Calculé (état dérivé)
  getTotalItems: () => number;
  getTotalPrice: () => number;
}
 
export const useCartStore = create<CartState>((set, get) => ({
  // État initial
  items: [],
  isOpen: false,
 
  // Actions
  addItem: (product) =>
    set((state) => {
      const existingItem = state.items.find(
        (item) => item.product.id === product.id
      );
 
      if (existingItem) {
        return {
          items: state.items.map((item) =>
            item.product.id === product.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        };
      }
 
      return { items: [...state.items, { product, quantity: 1 }] };
    }),
 
  removeItem: (productId) =>
    set((state) => ({
      items: state.items.filter((item) => item.product.id !== productId),
    })),
 
  updateQuantity: (productId, quantity) =>
    set((state) => ({
      items:
        quantity <= 0
          ? state.items.filter((item) => item.product.id !== productId)
          : state.items.map((item) =>
              item.product.id === productId ? { ...item, quantity } : item
            ),
    })),
 
  clearCart: () => set({ items: [] }),
 
  toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
 
  // Valeurs calculées utilisant get()
  getTotalItems: () =>
    get().items.reduce((total, item) => total + item.quantity, 0),
 
  getTotalPrice: () =>
    get().items.reduce(
      (total, item) => total + item.product.price * item.quantity,
      0
    ),
}));

Remarquez à quel point c'est propre comparé à Redux. Pas de types d'action, pas de reducers, pas de dispatch. Juste une fonction qui retourne un état et des actions.


Étape 4 : Utiliser le Store dans les Composants

Composant Carte Produit

Créez src/components/product-card.tsx :

// src/components/product-card.tsx
"use client";
 
import { useCartStore } from "@/stores/cart-store";
import type { Product } from "@/types/cart";
 
interface ProductCardProps {
  product: Product;
}
 
export function ProductCard({ product }: ProductCardProps) {
  const addItem = useCartStore((state) => state.addItem);
 
  return (
    <div className="rounded-lg border p-4 shadow-sm hover:shadow-md transition-shadow">
      <div className="aspect-square bg-gray-100 rounded-md mb-3 flex items-center justify-center">
        <span className="text-4xl">{product.image}</span>
      </div>
      <h3 className="font-semibold text-lg">{product.name}</h3>
      <p className="text-gray-500 text-sm">{product.category}</p>
      <div className="flex items-center justify-between mt-3">
        <span className="text-xl font-bold">{product.price.toFixed(2)} €</span>
        <button
          onClick={() => addItem(product)}
          className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
        >
          Ajouter au panier
        </button>
      </div>
    </div>
  );
}

Composant Badge Panier

Créez src/components/cart-badge.tsx :

// src/components/cart-badge.tsx
"use client";
 
import { useCartStore } from "@/stores/cart-store";
 
export function CartBadge() {
  // S'abonner uniquement à ce dont vous avez besoin - c'est la clé de la performance
  const totalItems = useCartStore((state) => state.getTotalItems());
  const toggleCart = useCartStore((state) => state.toggleCart);
 
  return (
    <button
      onClick={toggleCart}
      className="relative p-2 rounded-full hover:bg-gray-100"
    >
      <svg
        className="w-6 h-6"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 100 4 2 2 0 000-4z"
        />
      </svg>
      {totalItems > 0 && (
        <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
          {totalItems}
        </span>
      )}
    </button>
  );
}

Point clé : Remarquez comment nous utilisons (state) => state.getTotalItems() comme sélecteur. Zustand ne re-rend un composant que lorsque la valeur sélectionnée change. C'est une optimisation de performance automatique.


Étape 5 : Sélecteurs et Performance

Zustand ne re-rend les composants que lorsque l'état sélectionné change. C'est crucial pour la performance. Voici les patterns :

Mauvais : Sélectionner tout le store

// Ceci re-rend à CHAQUE changement d'état
const store = useCartStore();

Bon : Sélectionner des valeurs spécifiques

// Ne re-rend que quand items change
const items = useCartStore((state) => state.items);
 
// Ne re-rend que quand isOpen change
const isOpen = useCartStore((state) => state.isOpen);

Avancé : Fonctions d'égalité personnalisées

Pour les valeurs dérivées qui retournent de nouvelles références d'objet, utilisez la comparaison shallow :

import { useShallow } from "zustand/react/shallow";
 
// Ne re-rend que quand les valeurs réelles changent, pas la référence
const { items, isOpen } = useCartStore(
  useShallow((state) => ({
    items: state.items,
    isOpen: state.isOpen,
  }))
);

Créer des Sélecteurs Réutilisables

Créez src/stores/cart-selectors.ts :

// src/stores/cart-selectors.ts
import type { CartState } from "@/stores/cart-store";
 
export const selectCartItems = (state: CartState) => state.items;
export const selectIsCartOpen = (state: CartState) => state.isOpen;
export const selectCartTotal = (state: CartState) => state.getTotalPrice();
export const selectCartCount = (state: CartState) => state.getTotalItems();
 
export const selectItemById = (productId: string) => (state: CartState) =>
  state.items.find((item) => item.product.id === productId);

Utilisation :

const items = useCartStore(selectCartItems);
const total = useCartStore(selectCartTotal);
const item = useCartStore(selectItemById("product-1"));

Étape 6 : Middleware - Persistance

L'une des fonctionnalités les plus puissantes de Zustand est son système de middleware. Ajoutons la persistance pour sauvegarder le panier dans localStorage.

Mettez à jour src/stores/cart-store.ts :

// src/stores/cart-store.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import type { Product, CartItem } from "@/types/cart";
 
interface CartState {
  items: CartItem[];
  isOpen: boolean;
  addItem: (product: Product) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  toggleCart: () => void;
  getTotalItems: () => number;
  getTotalPrice: () => number;
}
 
export const useCartStore = create<CartState>()(
  persist(
    (set, get) => ({
      items: [],
      isOpen: false,
 
      addItem: (product) =>
        set((state) => {
          const existingItem = state.items.find(
            (item) => item.product.id === product.id
          );
          if (existingItem) {
            return {
              items: state.items.map((item) =>
                item.product.id === product.id
                  ? { ...item, quantity: item.quantity + 1 }
                  : item
              ),
            };
          }
          return { items: [...state.items, { product, quantity: 1 }] };
        }),
 
      removeItem: (productId) =>
        set((state) => ({
          items: state.items.filter((item) => item.product.id !== productId),
        })),
 
      updateQuantity: (productId, quantity) =>
        set((state) => ({
          items:
            quantity <= 0
              ? state.items.filter((item) => item.product.id !== productId)
              : state.items.map((item) =>
                  item.product.id === productId ? { ...item, quantity } : item
                ),
        })),
 
      clearCart: () => set({ items: [] }),
      toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
      getTotalItems: () =>
        get().items.reduce((total, item) => total + item.quantity, 0),
      getTotalPrice: () =>
        get().items.reduce(
          (total, item) => total + item.product.price * item.quantity,
          0
        ),
    }),
    {
      name: "cart-storage",
      storage: createJSONStorage(() => localStorage),
      // Persister uniquement les données, pas l'état UI comme isOpen
      partialize: (state) => ({ items: state.items }),
    }
  )
);

L'option partialize est importante : vous ne devriez persister que les données, pas l'état de l'interface comme l'ouverture du tiroir du panier.


Étape 7 : Gérer le SSR et l'Hydratation

Next.js App Router effectue d'abord le rendu des composants côté serveur. Comme localStorage n'existe pas côté serveur, nous devons gérer l'hydratation avec soin.

Créez src/stores/hydration.ts :

// src/stores/hydration.ts
"use client";
 
import { useEffect, useState } from "react";
 
export function useHydration() {
  const [hydrated, setHydrated] = useState(false);
 
  useEffect(() => {
    setHydrated(true);
  }, []);
 
  return hydrated;
}

Créez un composant de panier sécurisé pour l'hydratation dans src/components/cart-sidebar.tsx :

// src/components/cart-sidebar.tsx
"use client";
 
import { useCartStore } from "@/stores/cart-store";
import { useHydration } from "@/stores/hydration";
 
export function CartSidebar() {
  const hydrated = useHydration();
  const items = useCartStore((state) => state.items);
  const isOpen = useCartStore((state) => state.isOpen);
  const toggleCart = useCartStore((state) => state.toggleCart);
  const removeItem = useCartStore((state) => state.removeItem);
  const updateQuantity = useCartStore((state) => state.updateQuantity);
  const clearCart = useCartStore((state) => state.clearCart);
  const getTotalPrice = useCartStore((state) => state.getTotalPrice);
 
  if (!isOpen) return null;
 
  return (
    <div className="fixed inset-0 z-50 flex justify-end">
      {/* Fond sombre */}
      <div className="absolute inset-0 bg-black/50" onClick={toggleCart} />
 
      {/* Barre latérale */}
      <div className="relative w-full max-w-md bg-white shadow-xl p-6 overflow-y-auto">
        <div className="flex justify-between items-center mb-6">
          <h2 className="text-2xl font-bold">Votre Panier</h2>
          <button onClick={toggleCart} className="text-gray-500 hover:text-gray-700">

          </button>
        </div>
 
        {!hydrated ? (
          <div className="animate-pulse space-y-4">
            {[1, 2, 3].map((i) => (
              <div key={i} className="h-20 bg-gray-200 rounded" />
            ))}
          </div>
        ) : items.length === 0 ? (
          <p className="text-gray-500 text-center py-8">Votre panier est vide</p>
        ) : (
          <>
            <div className="space-y-4">
              {items.map((item) => (
                <div
                  key={item.product.id}
                  className="flex items-center gap-4 border-b pb-4"
                >
                  <span className="text-3xl">{item.product.image}</span>
                  <div className="flex-1">
                    <h3 className="font-medium">{item.product.name}</h3>
                    <p className="text-gray-500">
                      {item.product.price.toFixed(2)} €
                    </p>
                  </div>
                  <div className="flex items-center gap-2">
                    <button
                      onClick={() =>
                        updateQuantity(item.product.id, item.quantity - 1)
                      }
                      className="w-8 h-8 rounded-full border flex items-center justify-center"
                    >
                      -
                    </button>
                    <span className="w-8 text-center">{item.quantity}</span>
                    <button
                      onClick={() =>
                        updateQuantity(item.product.id, item.quantity + 1)
                      }
                      className="w-8 h-8 rounded-full border flex items-center justify-center"
                    >
                      +
                    </button>
                  </div>
                  <button
                    onClick={() => removeItem(item.product.id)}
                    className="text-red-500 hover:text-red-700"
                  >

                  </button>
                </div>
              ))}
            </div>
 
            <div className="mt-6 border-t pt-4">
              <div className="flex justify-between text-xl font-bold">
                <span>Total :</span>
                <span>{getTotalPrice().toFixed(2)} €</span>
              </div>
              <button className="w-full mt-4 bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition-colors">
                Passer à la caisse
              </button>
              <button
                onClick={clearCart}
                className="w-full mt-2 text-gray-500 hover:text-gray-700"
              >
                Vider le panier
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

Étape 8 : Store Slices pour les Applications Complexes

Au fur et à mesure que votre application grandit, vous voudrez diviser votre store en slices. Cela garde chaque partie focalisée et testable.

Créez src/stores/slices/cart-slice.ts :

// src/stores/slices/cart-slice.ts
import type { StateCreator } from "zustand";
import type { Product, CartItem } from "@/types/cart";
 
export interface CartSlice {
  items: CartItem[];
  addItem: (product: Product) => void;
  removeItem: (productId: string) => void;
  clearCart: () => void;
  getTotalItems: () => number;
  getTotalPrice: () => number;
}
 
export const createCartSlice: StateCreator<CartSlice> = (set, get) => ({
  items: [],
 
  addItem: (product) =>
    set((state) => {
      const existing = state.items.find((i) => i.product.id === product.id);
      if (existing) {
        return {
          items: state.items.map((i) =>
            i.product.id === product.id
              ? { ...i, quantity: i.quantity + 1 }
              : i
          ),
        };
      }
      return { items: [...state.items, { product, quantity: 1 }] };
    }),
 
  removeItem: (productId) =>
    set((state) => ({
      items: state.items.filter((i) => i.product.id !== productId),
    })),
 
  clearCart: () => set({ items: [] }),
 
  getTotalItems: () =>
    get().items.reduce((sum, i) => sum + i.quantity, 0),
 
  getTotalPrice: () =>
    get().items.reduce((sum, i) => sum + i.product.price * i.quantity, 0),
});

Créez src/stores/slices/ui-slice.ts :

// src/stores/slices/ui-slice.ts
import type { StateCreator } from "zustand";
 
export interface UISlice {
  isCartOpen: boolean;
  isSearchOpen: boolean;
  theme: "light" | "dark";
  toggleCart: () => void;
  toggleSearch: () => void;
  setTheme: (theme: "light" | "dark") => void;
}
 
export const createUISlice: StateCreator<UISlice> = (set) => ({
  isCartOpen: false,
  isSearchOpen: false,
  theme: "light",
 
  toggleCart: () => set((state) => ({ isCartOpen: !state.isCartOpen })),
  toggleSearch: () => set((state) => ({ isSearchOpen: !state.isSearchOpen })),
  setTheme: (theme) => set({ theme }),
});

Combinez les slices dans src/stores/app-store.ts :

// src/stores/app-store.ts
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { createCartSlice, type CartSlice } from "./slices/cart-slice";
import { createUISlice, type UISlice } from "./slices/ui-slice";
 
type AppStore = CartSlice & UISlice;
 
export const useAppStore = create<AppStore>()(
  persist(
    (...args) => ({
      ...createCartSlice(...args),
      ...createUISlice(...args),
    }),
    {
      name: "app-storage",
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({ items: state.items, theme: state.theme }),
    }
  )
);

Vous avez maintenant un store propre et modulaire qui évolue avec votre application.


Étape 9 : Devtools et Middleware de Logging

Empilez plusieurs middleware pour une excellente expérience de développement :

// src/stores/app-store.ts (version améliorée)
import { create } from "zustand";
import { persist, createJSONStorage, devtools } from "zustand/middleware";
import { createCartSlice, type CartSlice } from "./slices/cart-slice";
import { createUISlice, type UISlice } from "./slices/ui-slice";
 
type AppStore = CartSlice & UISlice;
 
export const useAppStore = create<AppStore>()(
  devtools(
    persist(
      (...args) => ({
        ...createCartSlice(...args),
        ...createUISlice(...args),
      }),
      {
        name: "app-storage",
        storage: createJSONStorage(() => localStorage),
        partialize: (state) => ({ items: state.items, theme: state.theme }),
      }
    ),
    { name: "AppStore" }
  )
);

Installez l'extension navigateur Redux DevTools pour inspecter votre store Zustand en temps réel. Chaque changement d'état apparaîtra avec les noms des actions et les diffs d'état.

Middleware de Logging Personnalisé

// src/stores/middleware/logger.ts
import type { StateCreator, StoreMutatorIdentifier } from "zustand";
 
type Logger = <
  T,
  Mps extends [StoreMutatorIdentifier, unknown][] = [],
  Mcs extends [StoreMutatorIdentifier, unknown][] = []
>(
  f: StateCreator<T, Mps, Mcs>,
  name?: string
) => StateCreator<T, Mps, Mcs>;
 
type LoggerImpl = <T>(
  f: StateCreator<T, [], []>,
  name?: string
) => StateCreator<T, [], []>;
 
const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => {
  const loggedSet: typeof set = (...args) => {
    const prev = get();
    set(...(args as Parameters<typeof set>));
    const next = get();
    if (process.env.NODE_ENV === "development") {
      console.log(`[${name ?? "store"}]`, { prev, next });
    }
  };
  return f(loggedSet, get, store);
};
 
export const logger = loggerImpl as unknown as Logger;

Étape 10 : Tout Assembler

Créez la page principale dans src/app/page.tsx :

// src/app/page.tsx
import { ProductCard } from "@/components/product-card";
import { CartBadge } from "@/components/cart-badge";
import { CartSidebar } from "@/components/cart-sidebar";
import type { Product } from "@/types/cart";
 
const products: Product[] = [
  { id: "1", name: "Casque Sans Fil", price: 79.99, image: "🎧", category: "Électronique" },
  { id: "2", name: "Clavier Mécanique", price: 149.99, image: "⌨️", category: "Électronique" },
  { id: "3", name: "Chaussures de Course", price: 129.99, image: "👟", category: "Sport" },
  { id: "4", name: "Machine à Café", price: 89.99, image: "☕", category: "Cuisine" },
  { id: "5", name: "Lampe de Bureau", price: 39.99, image: "💡", category: "Bureau" },
  { id: "6", name: "Sac à Dos", price: 59.99, image: "🎒", category: "Accessoires" },
];
 
export default function HomePage() {
  return (
    <div className="min-h-screen bg-gray-50">
      <header className="bg-white shadow-sm sticky top-0 z-40">
        <div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
          <h1 className="text-2xl font-bold">Zustand Store</h1>
          <CartBadge />
        </div>
      </header>
 
      <main className="max-w-7xl mx-auto px-4 py-8">
        <h2 className="text-3xl font-bold mb-8">Produits</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {products.map((product) => (
            <ProductCard key={product.id} product={product} />
          ))}
        </div>
      </main>
 
      <CartSidebar />
    </div>
  );
}

Étape 11 : Tester les Stores Zustand

Les stores Zustand sont de simples fonctions, ce qui les rend faciles à tester :

// src/stores/__tests__/cart-store.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { useCartStore } from "../cart-store";
 
const mockProduct = {
  id: "test-1",
  name: "Produit Test",
  price: 29.99,
  image: "🧪",
  category: "Test",
};
 
describe("Cart Store", () => {
  beforeEach(() => {
    // Réinitialiser le store entre les tests
    useCartStore.setState({ items: [] });
  });
 
  it("ajoute un article au panier", () => {
    useCartStore.getState().addItem(mockProduct);
    const items = useCartStore.getState().items;
 
    expect(items).toHaveLength(1);
    expect(items[0].product.id).toBe("test-1");
    expect(items[0].quantity).toBe(1);
  });
 
  it("incrémente la quantité pour les articles existants", () => {
    useCartStore.getState().addItem(mockProduct);
    useCartStore.getState().addItem(mockProduct);
 
    const items = useCartStore.getState().items;
    expect(items).toHaveLength(1);
    expect(items[0].quantity).toBe(2);
  });
 
  it("supprime un article du panier", () => {
    useCartStore.getState().addItem(mockProduct);
    useCartStore.getState().removeItem("test-1");
 
    expect(useCartStore.getState().items).toHaveLength(0);
  });
 
  it("calcule le prix total correctement", () => {
    useCartStore.getState().addItem(mockProduct);
    useCartStore.getState().addItem(mockProduct);
 
    expect(useCartStore.getState().getTotalPrice()).toBe(59.98);
  });
 
  it("vide entièrement le panier", () => {
    useCartStore.getState().addItem(mockProduct);
    useCartStore.getState().clearCart();
 
    expect(useCartStore.getState().items).toHaveLength(0);
  });
});

Remarquez comment vous accédez directement au store avec getState() et setState() en dehors des composants React. Pas besoin d'utilitaires de rendu ni de providers.


Dépannage

Avertissements de désynchronisation d'hydratation

Si vous voyez des désynchronisations d'hydratation avec les stores persistants, assurez-vous que les composants client utilisant un état persistant affichent un squelette de chargement jusqu'à l'hydratation :

const hydrated = useHydration();
if (!hydrated) return <Skeleton />;

Le store ne se met pas à jour entre les onglets

Ajoutez l'écouteur d'événement storage pour la synchronisation entre onglets :

persist(
  // ... configuration du store
  {
    name: "cart-storage",
    storage: createJSONStorage(() => localStorage),
    // Ceci active la synchronisation entre onglets
    skipHydration: false,
  }
)

Problèmes de performance avec les grands stores

Utilisez des sélecteurs granulaires au lieu de sélectionner le store entier. Utilisez useShallow lorsque vous sélectionnez plusieurs valeurs qui retournent de nouvelles références d'objet.


Prochaines Étapes

Maintenant que vous maîtrisez Zustand avec Next.js App Router, voici quelques pistes pour aller plus loin :

  • Ajoutez le middleware Immer pour des mises à jour d'état imbriqué plus ergonomiques
  • Construisez une wishlist en utilisant un slice de store séparé
  • Intégrez une vraie API en utilisant les actions asynchrones de Zustand
  • Ajoutez des mises à jour optimistes pour les mutations serveur
  • Explorez subscribeWithSelector de Zustand pour les effets de bord hors React

Conclusion

Zustand offre l'équilibre parfait entre simplicité et puissance pour la gestion d'état React en 2026. Son API minimale, son excellent support TypeScript et son intégration transparente avec Next.js App Router en font le choix idéal pour les applications modernes.

Vous avez appris à créer des stores, utiliser des sélecteurs pour la performance, ajouter la persistance, gérer l'hydratation SSR, organiser le code avec des slices et tout tester. Ces patterns fonctionnent aussi bien pour de simples applications compteur que pour des plateformes e-commerce complexes.

Le point clé à retenir : commencez simplement avec un seul store, ajoutez des middleware selon les besoins, et divisez en slices uniquement quand la complexité l'exige. Zustand évolue avec votre application sans jamais vous gêner.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Embeddings d'articles Wikipedia pour la recherche.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire un Chatbot IA Local avec Ollama et Next.js : Guide Complet

Construisez un chatbot IA privé fonctionnant entièrement sur votre machine locale avec Ollama et Next.js. Ce tutoriel pratique couvre l'installation, le streaming des réponses, la sélection de modèles et le déploiement d'une interface de chat prête pour la production — le tout sans envoyer de données dans le cloud.

25 min read·