Guide de choix de gestion d'état Next.js : Zustand vs Jotai en pratique

Le message d’erreur réapparaît — 17e modification de l’action creator Redux. Le projet n’a qu’un panier, et vous avez déjà écrit trois fichiers de configuration.
Pourquoi autant de complexité ? Projet modeste, Redux semble excessif ; on passe à Context API, et une seule mise à jour d’état re-rend la moitié de la page. Le panneau de performance tout rouge, ça donne mal à la tête.
C’est pour ça que Zustand et Jotai ont explosé ces deux dernières années — ils promettent légèreté et hautes performances. Mais lequel choisir ? Cet article aborde :
- Pourquoi Redux et Context posent problème (les vrais pièges)
- La différence fondamentale entre Zustand et Jotai
- Quel outil choisir selon le scénario (arbre de décision)
- Comment les utiliser dans Next.js App Router (retours d’expérience)
Pourquoi pas Redux ni Context ?
La « lourdeur » de Redux, concrètement
Commençons par Redux. Ce n’est pas mauvais — c’est juste excessif pour beaucoup de projets.
Il faut écrire action types, action creators, reducers, et configurer le store. Une simple fonction « ajouter au panier » peut toucher trois ou quatre fichiers. Ce boilerplate, à force, ça lasse.
Surtout, si l’équipe compte des juniors, la courbe d’apprentissage Redux est raide. C’est quoi dispatch ? Pourquoi une pure function ? À quoi sert middleware ? Ces concepts demandent du temps.
Pour une todo list ou un blog perso, Redux c’est comme conduire un char pour faire les courses — vous y arrivez, mais ce n’est pas nécessaire.
Le piège performance de Context API
Et Context ? Simple, natif, aucune bibliothèque à installer.
Mais gros problème : les performances.
Context fonctionne ainsi : dès que la value du Provider change, tous les composants consommateurs se re-rendent. Même si vous n’utilisez qu’un seul champ, le composant entier repasse par le cycle de rendu.
J’ai fait un formulaire géré par Context. Un onChange dans un champ a re-rendu 20 composants. La flame chart Chrome DevTools, c’était pénible à regarder.
On peut optimiser avec memo, useMemo, diviser les Context — mais honnêtement, le code optimisé n’est pas plus simple que Redux.
L’attrait des solutions légères
Voilà pourquoi Zustand et Jotai sont si populaires.
Leur promesse est claire :
- API simple, prise en main rapide (Zustand en 10 minutes)
- Optimisations de performance intégrées, sans bricolage manuel
- Petit bundle (Zustand ~1 KB, encore moins en gzip)
Les chiffres le confirment. En 2025, l’usage de Zustand a progressé de 150 % sur un an. De plus en plus de développeurs quittent Redux pour ces alternatives légères.
Mais nouvelle question : Zustand ou Jotai ?
Zustand vs Jotai : différences clés
En surface, les deux sont des solutions « légères » — mais la conception sous-jacente diffère totalement.
Modèle d’état : magasin unique vs étals atomiques
La doc officielle résume bien : « Zustand is like Redux. Jotai is like Recoil. »
Zustand est essentiellement un Redux simplifié. Un store, tout l’état dedans. Comme un grand centre commercial.
// Zustand : un grand store
const useStore = create((set) => ({
user: null,
cart: [],
theme: 'light',
// tout l'état ici
}))
Jotai est atomique. Chaque état est un atom indépendant, comme des petits stands autonomes.
// Jotai : atoms indépendants
const userAtom = atom(null)
const cartAtom = atom([])
const themeAtom = atom('light')
Cette différence de design détermine directement les cas d’usage.
Emplacement : hors module vs dans l’arbre de composants
Le store Zustand est au niveau module, en dehors de React. Importable et modifiable partout, sans Provider.
Les atoms Jotai vivent dans l’arbre de composants, via Context. Il faut un Provider à la racine pour partager l’état.
Conséquence ?
Pour mettre à jour l’état hors des composants React (utilitaire, callback WebSocket), Zustand est bien plus pratique. Jotai le permet aussi, mais avec des contournements.
Performances : optimisation manuelle vs automatique
Stratégies différentes.
L’abonnement atomique de Jotai est optimal par défaut. Un composant ne s’abonne qu’aux atoms qu’il utilise ; les autres mises à jour ne déclenchent pas de re-render.
Zustand demande des selectors :
// Déconseillé : s'abonner à tout le store
const store = useStore()
// Recommandé : selector pour la partie nécessaire
const user = useStore(state => state.user)
Les selectors Zustand restent simples et intuitifs. Il faut juste s’en souvenir, sinon piège garanti.
En résumé
- Zustand : store unique, hors React, selectors manuels
- Jotai : atoms atomiques, dans React, performances auto-optimisées
Le choix dépend des caractéristiques du projet.
Quand choisir Zustand ?
Si votre projet correspond à ces critères, Zustand est généralement le meilleur choix.
Applications PME, sans sur-complexifier
Honnêtement, la plupart des projets n’ont pas besoin de gestion d’état complexe.
Un site e-commerce : état global typique = utilisateur, panier, thème. Zustand est parfait.
L’API est si simple qu’elle se apprend quasi sans effort. Exemple complet :
// store.js
import create from 'zustand'
const useStore = create((set) => ({
cart: [],
addToCart: (item) => set((state) => ({
cart: [...state.cart, item]
})),
removeFromCart: (id) => set((state) => ({
cart: state.cart.filter(item => item.id !== id)
})),
}))
// CartButton.jsx
function CartButton() {
const addToCart = useStore(state => state.addToCart)
return <button onClick={() => addToCart(item)}>Ajouter au panier</button>
}
// CartCount.jsx
function CartCount() {
const count = useStore(state => state.cart.length)
return <span>{count}</span>
}
Pas de Provider, pas d’action types, pas de reducer. On définit état et méthodes, on utilise.
Un junior comprend ce code en 10 minutes.
Mise à jour d’état hors React
Avantage distinctif de Zustand.
Connexion WebSocket, message entrant à traiter :
// websocket.js
import { useStore } from './store'
socket.on('message', (data) => {
// Appel direct, sans passer par un composant
useStore.getState().updateMessages(data)
})
Ou dans un utilitaire, décision basée sur l’état courant :
// utils.js
import { useStore } from './store'
export function checkPermission() {
const user = useStore.getState().user
return user?.role === 'admin'
}
Jotai est moins naturel ici — les atoms sont liés à l’arbre de composants.
Compatible SSR Next.js
Le support Zustand dans Next.js est très mature.
La doc officielle a un chapitre dédié à l’intégration Next.js et aux bonnes pratiques App Router. La communauté a documenté les pièges — en cas de problème, on trouve généralement une solution.
Avec Next.js 13+ App Router, Zustand est l’un des choix les plus sûrs. Configuration détaillée plus bas.
Quand Zustand n’est pas optimal ?
Scénario limite : dépendances dérivées complexes entre états.
Filtre avec 10 critères, chaque liste d’options dépendant des autres — le code Zustand devient tortueux.
Là, le design atomique de Jotai brille.
Quand choisir Jotai ?
Le design atomique de Jotai est remarquable dans certains scénarios.
Dépendances d’état complexes
Le domaine de prédilection de Jotai.
Filtre produits :
- critères « marque », « fourchette de prix », « note »
- marques disponibles selon la fourchette de prix
- liste finale déterminée par tous les critères
En Zustand, vous gérez ces dépendances à la main — code facilement confus.
Avec Jotai, c’est clair :
// Atoms de base
const brandAtom = atom([])
const priceRangeAtom = atom([0, 1000])
const ratingAtom = atom(0)
// Atom dérivé : marques disponibles (dépend du prix)
const availableBrandsAtom = atom((get) => {
const priceRange = get(priceRangeAtom)
return fetchBrands(priceRange) // réagit automatiquement au changement
})
// Atom dérivé : produits filtrés (dépend de tous les critères)
const filteredProductsAtom = atom((get) => {
const brands = get(brandAtom)
const priceRange = get(priceRangeAtom)
const rating = get(ratingAtom)
return products.filter(/* logique de filtre */)
})
Chaque atom ne s’intéresse qu’à ses dépendances ; Jotai trace automatiquement. Ce qui change met à jour les atoms concernés.
Dans les composants :
function FilterPanel() {
const [brands, setBrands] = useAtom(brandAtom)
const availableBrands = useAtomValue(availableBrandsAtom)
// brands change → availableBrands recalculé automatiquement
}
Dans ce scénario, Jotai est bien plus lisible que Zustand.
Performances extrêmes
L’abonnement atomique de Jotai performe vraiment.
Tableau de bord temps réel, 50 composants, chacun affichant une métrique différente. Avec Context ou Zustand non optimisé, une mise à jour peut re-rendre une foule de composants.
Pas Jotai. Chaque composant ne s’abonne qu’à son atom.
La doc officielle est explicite : « This is the most performant by default. » Seul l’atom abonné déclenche un re-render quand il change.
Grandes applications avec code splitting
Les atoms Jotai se chargent à la demande.
Atoms dispersés dans différents fichiers, importés seulement quand nécessaire — utile pour le first paint des grandes apps.
Le store Zustand est souvent monolithique ; la division est possible, mais moins naturelle.
Projets utilisant intensivement Suspense
Si vous exploitez React Suspense (chargement asynchrone), Jotai a un support natif.
Atom asynchrone intuitif :
const userAtom = atom(async () => {
const res = await fetch('/api/user')
return res.json()
})
function UserProfile() {
const user = useAtomValue(userAtom)
// Suspense automatique, rendu après chargement
return <div>{user.name}</div>
}
Zustand peut s’associer à Suspense, mais avec du wrapping supplémentaire — moins fluide que Jotai.
Coût d’apprentissage de Jotai
Jotai reste un peu plus conceptuel que Zustand.
Lecture/écriture d’atoms, atoms dérivés, atoms async — concepts à assimiler. Pour une équipe junior, prise en main plus lente.
Et la doc Jotai, franchement, est moins accueillante que celle de Zustand. Il faut relire pour maîtriser les APIs.
Bonnes pratiques Next.js App Router
Assez de théorie — passons au concret. Avec Next.js 13+ App Router, voici les pièges à connaître.
Zustand dans Next.js : la bonne approche
Gros piège : ne pas utiliser de store global
Beaucoup (moi inclus au début) écrivent :
// ❌ Erreur : store global
import create from 'zustand'
const useStore = create((set) => ({
user: null,
setUser: (user) => set({ user })
}))
Ça marche en CSR, mais en SSR Next.js ce store est partagé entre requêtes. Les données de l’utilisateur A peuvent apparaître chez B — faille de sécurité.
La doc recommande le pattern Store Factory :
// lib/store.js
import { createStore } from 'zustand/vanilla'
export function createUserStore(initialState) {
return createStore((set) => ({
user: initialState?.user || null,
setUser: (user) => set({ user })
}))
}
Puis Provider dans un Client Component :
// components/StoreProvider.jsx
'use client'
import { createContext, useContext, useRef } from 'react'
import { useStore } from 'zustand'
import { createUserStore } from '@/lib/store'
const StoreContext = createContext(null)
export function StoreProvider({ children, initialState }) {
const storeRef = useRef()
if (!storeRef.current) {
storeRef.current = createUserStore(initialState)
}
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
)
}
export function useUserStore(selector) {
const store = useContext(StoreContext)
return useStore(store, selector)
}
Dans le layout racine :
// app/layout.jsx
import { StoreProvider } from '@/components/StoreProvider'
export default function RootLayout({ children }) {
// Préchargement possible ici
const initialState = { user: null }
return (
<html>
<body>
<StoreProvider initialState={initialState}>
{children}
</StoreProvider>
</body>
</html>
)
}
Chaque requête a son store — pas de mélange de données.
Points d’attention :
- Les Server Components ne lisent/écrivent pas le store directement
- Préchargement côté serveur, passage via initialState au client
- Évitez le fetch bloquant dans le layout racine — impact performance
Hydration SSR avec Jotai
Le plus gros piège Jotai dans Next.js : erreurs d’hydration.
Provider indépendant par requête obligatoire :
// app/providers.jsx
'use client'
import { Provider } from 'jotai'
export function Providers({ children }) {
return <Provider>{children}</Provider>
}
// app/layout.jsx
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
Données serveur :
Pour injecter des données serveur dans les atoms, useHydrateAtoms :
'use client'
import { useHydrateAtoms } from 'jotai/utils'
import { userAtom } from '@/atoms'
export function HydrateAtoms({ initialUser, children }) {
useHydrateAtoms([[userAtom, initialUser]])
return children
}
Piège : useHydrateAtoms ne s’applique qu’au premier rendu. Avec router.push dans App Router, la deuxième visite du même atom ne se re-hydrate pas.
Solutions :
- Placer le Provider dans
template.tsxplutôt quelayout.tsx(recréé à chaque navigation) - Ou Provider au niveau page, pas global
Erreur d’hydration avec atomWithStorage :
atomWithStorage pour des données de formulaire peut provoquer une incohérence SSR/client :
- Serveur : formulaire vide
- Client : valeur lue depuis localStorage
- Résultat : React hydration mismatch
Solution : remplir dans useEffect, ou useHydrateAtoms + useSyncExternalStore.
Principes communs (les deux bibliothèques)
-
Server Components sans gestion d’état
- Pas de hooks, pas de
useStoreniuseAtom - Composants avec état →
'use client'
- Pas de hooks, pas de
-
Emplacement du Provider
- Plus profond possible, pour optimiser les parties statiques
- Mais accessible à tous les composants concernés
-
Éviter de bloquer le layout racine
- Pas de
await fetch()utilisateur dans root layout - Cela annule les gains du streaming et des Server Components
- Client Component dédié pour fetch et initialisation
- Pas de
Ces pièges, je les ai tous rencontrés. Ces principes font gagner des heures de debug.
Mes recommandations
Après tout ça, la question reste : lequel choisir ?
Pas de réponse universelle. Voici un arbre de décision.
Arbre de décision rapide
Si votre projet est…
-
Projet perso simple / petite app
→ Context API d’abord, tant que ça suffit
→ Problème de perf → Zustand -
SaaS moyen / e-commerce
→ Zustand directement
→ Simple, stable, équipe à l’aise rapidement -
Tableau de bord complexe / app temps réel
→ Jotai
→ Dépendances d’état complexes, code plus clair -
Grande équipe / normes strictes
→ Redux Toolkit peut convenir
→ Plus de contraintes et de bonnes pratiques -
Mise à jour d’état hors React
→ Zustand
→ Jotai possible, mais moins naturel -
Suspense intensif
→ Jotai plus fluide
→ Atoms async natifs
Stratégie progressive
Je recommande une approche progressive :
-
Phase 1 : Context API
- Démarrage, peu d’état, Context suffit
- Ne changez pas tant que ça marche — pas d’optimisation prématurée
-
Phase 2 : Zustand
- Context ralentit
- Ou état global plus complexe
- Zustand couvre ~80 % des cas
-
Phase 3 : Jotai ou rester sur Zustand
- Dépendances très complexes → Jotai
- Sinon restez sur Zustand
- Pas de changement pour la technologie elle-même
Peut-on mixer ?
Oui !
Zustand et Jotai ne s’excluent pas. J’ai vu des projets :
- Zustand pour config globale (utilisateur, thème)
- Jotai pour formulaires complexes
Aucun problème. Choisissez l’outil adapté au problème concret.
Mon expérience
Mes choix perso :
- Blog perso : pas de gestion d’état, Server Components + URL state suffisent
- Back-office : Zustand + React Query pour l’état serveur
- Tableau de bord temps réel : Jotai, dépendances trop complexes pour Zustand
Ne vous obsédez pas sur le choix technique dès le départ. Commencez simple, upgradez si besoin.
Beaucoup de projets, Context API suffit. Ne le sous-estimez pas.
Conclusion
Retour à la question initiale.
Redux trop lourd ? Oui, excessif pour beaucoup de projets.
Context performant ? Non, sans optimisation les re-renders deviennent un goulot.
Zustand ou Jotai ? Selon le contexte :
- La plupart du temps, Zustand suffit — simple et stable
- Dépendances complexes, Jotai plus élégant
- Incertain ? Zustand d’abord, changez si nécessaire
Next.js App Router ? Trois règles :
- Pas de store global
- Provider indépendant par requête
- Server Components sans état
Dernier mot.
Pas de réponse standard en choix technique. Ne vous laissez pas guider aveuglément par les comparatifs en ligne — y compris celui-ci. L’essentiel : une solution confortable pour vous et l’équipe, qui résout le problème réel.
Si vous hésitez : choisissez-en un, faites un demo, testez. Dix minutes de pratique valent dix articles.
Bon choix d’outil.
FAQ
Quelle différence entre Redux, Context, Zustand et Jotai ?
• Avantages : puissant, écosystème riche, adapté aux très grands projets
• Inconvénients : trop lourd, beaucoup de code boilerplate, courbe d'apprentissage élevée
• Usage : très grands projets, gestion d'état complexe
Context API :
• Avantages : intégré à React, pas de bibliothèque supplémentaire
• Inconvénients : performances médiocres, une mise à jour peut re-rendre tout le sous-arbre
• Usage : état global simple, mises à jour peu fréquentes
Zustand :
• Avantages : simple et direct, peu de code, faible courbe d'apprentissage
• Inconvénients : moins adapté aux très grands projets
• Usage : la plupart des projets, PME
Jotai :
• Avantages : état atomique, mises à jour granulaires, bonnes performances
• Inconvénients : courbe d'apprentissage plus élevée, code plus complexe
• Usage : grands projets, performances maximales
Conseil : Zustand pour la plupart des projets ; Jotai pour les grands projets ou les performances extrêmes.
Quand utiliser Zustand, quand utiliser Jotai ?
• La plupart des projets
• Projets petits à moyens
• Besoin d'une gestion d'état simple et directe
• Faible coût d'apprentissage pour l'équipe
• Peu de code
Choisir Jotai quand :
• Grands projets
• Performances maximales requises
• Mises à jour d'état fréquentes
• Besoin de mises à jour granulaires
• Équipe expérimentée
Arbre de décision :
• Petit projet → Zustand
• Grand projet → Jotai
• Exigences de performance élevées → Jotai
• Simplicité → Zustand
Conseil : Zustand pour la plupart ; Jotai seulement pour les grands projets ou les performances extrêmes.
Pourquoi Context API a de mauvaises performances ?
Solutions :
• Diviser les Context (séparer par fonctionnalité)
• Optimiser avec useMemo et memo
• Passer à Zustand ou Jotai (abonnements granulaires)
Conseil : si l'état se met à jour souvent, évitez Context API ; préférez Zustand ou Jotai.
Comment utiliser Zustand dans Next.js App Router ?
1. Utiliser createStore pour créer une factory de store
2. Dans un Client Component, créer l'instance avec useRef
3. Passer l'instance aux composants enfants via Context Provider
Points clés :
• Les composants utilisant le store doivent être marqués 'use client'
• Les Server Components ne peuvent pas utiliser la gestion d'état
• Chaque requête doit avoir son propre store, pour éviter le mélange de données
Cela garantit un état indépendant par requête tout en conservant la simplicité de Zustand.
Comment utiliser Jotai dans Next.js App Router ?
1. Envelopper avec le Provider dans le layout racine ou template.jsx
2. Utiliser useHydrateAtoms pour injecter les données serveur comme valeurs initiales
3. Attention : useHydrateAtoms ne s'applique qu'au premier rendu
Points clés :
• Les composants utilisant des atoms doivent être des Client Components
• Les Server Components ne peuvent pas utiliser la gestion d'état
• Design atomique, mises à jour granulaires automatiques
Avantages :
• Seuls les composants abonnés à un atom spécifique se mettent à jour
• Support natif de Suspense et des données asynchrones
• Adapté aux dépendances d'état complexes et aux grands projets
Note : Jotai a un coût d'apprentissage plus élevé que Zustand, mais offre de meilleures performances et une meilleure clarté dans les scénarios complexes.
Les Server Components peuvent-ils utiliser la gestion d'état ?
Approche correcte :
• Server Components pour la récupération de données (fetch, requêtes BDD, etc.)
• Passer les données aux Client Components via props
• Client Components pour la gestion d'état et l'interaction utilisateur
Pattern d'architecture :
Server Component récupère les données initiales → passe via props → Client Component gère l'état et l'interaction avec Zustand/Jotai
Cette répartition laisse les composants serveur à la récupération de données et au SEO, et les composants client à l'interaction et à la gestion d'état — exploitant pleinement Next.js App Router.
12 min de lecture · Publié le: 19 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide complet Next.js
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Guide complet Next.js + Prisma : de la configuration à la pratique (avec solution aux fuites de connexion)
Tutoriel complet Next.js + Prisma : configuration, conception du Schema, CRUD en pratique et solution aux fuites de connexion au hot reload, pour démarrer rapidement avec Prisma ORM.
Partie 23 sur 51
Suivant
Guide complet du cache Next.js : maîtriser le bon moment pour utiliser revalidate
Plongée dans les quatre couches de cache Next.js, maîtrisez revalidate, revalidatePath et revalidateTag, résolvez les données obsolètes avec un guide de dépannage complet et des bonnes pratiques
Partie 25 sur 51



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire