logo
Zustand avec persist dans Next.js : guide complet pour débutants
Tous les articles
Tutoriel

Zustand avec persist dans Next.js : guide complet pour débutants

C
Claudin·30 juin 2026·3 min de lecture

Si tu développes avec Next.js et que tu en as marre de passer des props à travers 5 niveaux de composants juste pour partager une donnée, ou si Redux te paraît trop lourd pour ton projet, Zustand est probablement la solution qu'il te faut. Dans cet article, on va tout reprendre depuis le début : pourquoi utiliser Zustand, comment l'installer, comment créer ton premier store, comment ajouter la persistance avec localStorage, et surtout comment éviter le piège classique de l'hydratation avec Next.js.

1. Pourquoi Zustand plutôt que Redux ou Context API ?

Avant de foncer dans le code, comprenons le problème qu'on essaie de résoudre. Quand une application grandit, tu as souvent besoin de partager des données entre plusieurs composants qui n'ont pas de lien direct entre eux. Par exemple : un panier d'achat que tu veux afficher dans le header, dans la page produit, et dans la page de checkout. Trois solutions existent classiquement : Le prop drilling : tu passes la donnée de composant en composant via des props. Ça fonctionne mais devient vite ingérable si tu as 5 ou 6 niveaux de composants imbriqués. Le Context API de React : intégré nativement, mais verbeux à mettre en place (Provider, Context, hook personnalisé) et peut entraîner des re-renders inutiles si mal utilisé. Redux : très puissant, mais nécessite beaucoup de boilerplate (actions, reducers, dispatch, slices...) pour des besoins parfois très simples. Zustand se positionne entre les deux : aussi simple que le Context API à utiliser, sans les inconvénients de performance, et sans le boilerplate de Redux. Un store Zustand, c'est littéralement une fonction JavaScript que tu peux appeler depuis n'importe quel composant.

2. Installation du package

Dans ton projet Next.js existant, ouvre ton terminal et installe Zustand :

bash
npm install zustand

3. Créer ton premier store

Un "store" dans Zustand, c'est l'endroit où tu vas définir ta donnée et les fonctions qui permettent de la modifier. Crée un dossier stores à la racine de ton projet (ou dans src si tu utilises cette structure), puis un fichier dedans. Structure recommandée :

text
src/
  stores/
    useCounterStore.ts
  app/
    page.tsx

Contenu du fichier useCounterStore.ts :

tsx
import { create } from 'zustand'

// 1. On définit le type de notre état
interface CounterState {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

// 2. On crée le store avec create()
export const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}))

Explication ligne par ligne pour ceux qui découvrent :

create<CounterState>(...) crée le store. Le <CounterState> indique à TypeScript la forme exacte de notre état, pour avoir l'autocomplétion et éviter les erreurs de typage. set est une fonction fournie par Zustand qui te permet de modifier l'état. Tu ne modifies jamais l'état directement, tu passes toujours par set. count: 0 est la valeur initiale de notre compteur. increment: () => set((state) => ({ count: state.count + 1 })) : on définit une fonction qui, quand on l'appelle, met à jour count en se basant sur sa valeur précédente (state.count).

4. Utiliser le store dans un composant

Maintenant, utilisons ce store dans un composant . Comme on touche à de l'état interactif, ce composant doit être un Client Component (donc avec 'use client' en haut du fichier, obligatoire dans Next.js App Router).

tsx
'use client'

import { useCounterStore } from '@/stores/useCounterStore'

export function Counter() {
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)
  const decrement = useCounterStore((state) => state.decrement)

  return (
    <div>
      <p>Compteur : {count}</p>
      <button onClick={increment}>+ 1</button>
      <button onClick={decrement}>- 1</button>
    </div>
  )
}

5. Ajouter la persistance avec le middleware persist

Pour l'instant, si l'utilisateur rafraîchit la page, le compteur revient à 0. C'est là qu'intervient le middleware persist : il sauvegarde automatiquement ton store dans le localStorage du navigateur, et le recharge automatiquement au démarrage de l'app. Prenons un cas plus concret et utile : un panier d'achat. Création du store avec persist, fichier useCartStore.ts :

tsx
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

interface Product {
  id: string
  name: string
  price: number
}

interface CartState {
  items: Product[]
  addItem: (product: Product) => void
  removeItem: (id: string) => void
  clearCart: () => void
}

export const useCartStore = create<CartState>()(
  persist(
    (set) => ({
      items: [],
      addItem: (product) =>
        set((state) => ({ items: [...state.items, product] })),
      removeItem: (id) =>
        set((state) => ({
          items: state.items.filter((item) => item.id !== id),
        })),
      clearCart: () => set({ items: [] }),
    }),
    {
      name: 'cart-storage', // nom de la clé créée dans le localStorage
      storage: createJSONStorage(() => localStorage),
    }
  )
)

6. Le piège classique : l'erreur d'hydratation avec Next.js

C'est le point sur lequel presque tout le monde se fait avoir la première fois, donc on va prendre le temps de bien comprendre. Le problème : Next.js fait du rendu côté serveur (SSR). Or, le serveur n'a pas accès au localStorage du navigateur, c'est une API qui n'existe que côté client. Donc au premier rendu sur le serveur, Zustand utilise la valeur initiale (items: []). Ensuite, côté client, Zustand va lire le localStorage et restaurer les vraies données. Résultat : pendant une fraction de seconde, le HTML généré par le serveur ("panier vide") ne correspond pas à ce que React veut afficher côté client ("panier avec 3 articles"). React détecte cette différence et lève une erreur dans la console :

text
Warning: Text content did not match. Server: "0 articles" Client: "3 articles"
0 commentaire
À lire aussi

Articles similaires