State Management React 2026 : Zustand vs Jotai vs Recoil - Le Guide Complet
Redux est mort. Longue vie à Zustand, Jotai et Recoil.
En 2026, le paysage du state management React a radicalement changé. Redux, autrefois incontournable, est aujourd'hui considéré comme over-engineered pour 90% des projets.
Ce guide compare les 3 alternatives modernes - Zustand, Jotai, Recoil - avec des benchmarks réels, patterns concrets et recommandations projet par projet.
TL;DR : Quelle Librairie Choisir ?
| Use Case | Recommandation | Pourquoi |
|---|---|---|
| Nouveau projet simple | Zustand | Simple, rapide, peu de boilerplate |
| App complexe React 19 | Jotai | Atomic state, React Suspense native |
| Migration Redux | Zustand | API similaire, migration facile |
| State dérivé complexe | Jotai | Computed values élégants |
| Meta framework (Next.js) | Zustand | SSR-friendly out-of-the-box |
| Projet legacy | Redux Toolkit | Seulement si déjà en place |
Comparatif Technique : Le Tableau Complet
| Critère | Zustand | Jotai | Recoil | Redux Toolkit |
|---|---|---|---|---|
| Bundle size | 1.2 KB | 3.1 KB | 79 KB | 45 KB |
| Boilerplate | 🟢 Minimal | 🟢 Minimal | 🟡 Moyen | 🔴 Élevé |
| TypeScript | ✅ Excellent | ✅ Excellent | ⚠️ Bon | ✅ Excellent |
| DevTools | ⚠️ Basique | ✅ Excellent | ✅ Excellent | ✅ Excellent |
| SSR (Next.js) | ✅ Natif | ⚠️ Config | ⚠️ Config | ⚠️ Config |
| React Suspense | ❌ Non | ✅ Natif | ✅ Natif | ❌ Non |
| Learning curve | 🟢 Facile | 🟡 Moyen | 🟡 Moyen | 🔴 Difficile |
| Performance | 🚀 Excellent | 🚀 Excellent | ✅ Très bon | ✅ Très bon |
| Ecosystem | 🌿 Croissant | 🌱 Naissant | 🌱 Naissant | 🌍 Énorme |
| Maintenance | ✅ Actif | ✅ Actif | ⚠️ Meta only | ✅ Actif |
Zustand : Le Pragmatique
Philosophie
"State management aussi simple que useState, mais global et performant."
Créé par : Poimandres (même team que React Three Fiber, Jotai)
Première version : 2019
GitHub stars : 47k+
Exemple Concret : Todo App
// store/todo-store.ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
type Todo = {
id: string
text: string
completed: boolean
}
type TodoStore = {
todos: Todo[]
addTodo: (text: string) => void
toggleTodo: (id: string) => void
removeTodo: (id: string) => void
}
export const useTodoStore = create<TodoStore>()(
devtools(
persist(
(set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { id: crypto.randomUUID(), text, completed: false }]
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter(todo => todo.id !== id)
}))
}),
{ name: 'todo-storage' }
)
)
)
// components/TodoList.tsx
import { useTodoStore } from '@/store/todo-store'
export function TodoList() {
// ✅ Sélecteur granulaire (re-render uniquement si todos change)
const todos = useTodoStore(state => state.todos)
const toggleTodo = useTodoStore(state => state.toggleTodo)
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span className={todo.completed ? 'line-through' : ''}>
{todo.text}
</span>
</li>
))}
</ul>
)
}
// components/AddTodo.tsx
export function AddTodo() {
const addTodo = useTodoStore(state => state.addTodo)
const [text, setText] = useState('')
return (
<form onSubmit={(e) => {
e.preventDefault()
addTodo(text)
setText('')
}}>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">Add</button>
</form>
)
}
Avantages Zustand
- ✅ Bundle size : 1.2 KB (vs 45 KB Redux Toolkit)
- ✅ Zero boilerplate : Pas d'actions, reducers, dispatch
- ✅ TypeScript : Inference automatique
- ✅ Middleware : persist, devtools, immer inclus
- ✅ SSR : Fonctionne out-of-the-box Next.js
- ✅ Sélecteurs : Re-renders optimisés automatiquement
Inconvénients
- ⚠️ DevTools basiques : Moins puissant que Redux DevTools
- ⚠️ Pas de time-travel par défaut
- ⚠️ Structure libre : Peut devenir désordonné sur gros projets
Jotai : L'Atomic State
Philosophie
"Atomic state management with React Suspense built-in."
Créé par : Daishi Kato (Poimandres)
Première version : 2020
GitHub stars : 18k+
Exemple Concret : Todo App
// atoms/todo-atoms.ts
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
type Todo = {
id: string
text: string
completed: boolean
}
// ✅ Atomic state : chaque piece de state est un atom
export const todosAtom = atomWithStorage<Todo[]>('todos', [])
// ✅ Derived state (computed values)
export const todoStatsAtom = atom((get) => {
const todos = get(todosAtom)
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
}
})
// ✅ Write-only atoms (actions)
export const addTodoAtom = atom(
null,
(get, set, text: string) => {
const todos = get(todosAtom)
set(todosAtom, [
...todos,
{ id: crypto.randomUUID(), text, completed: false }
])
}
)
export const toggleTodoAtom = atom(
null,
(get, set, id: string) => {
const todos = get(todosAtom)
set(todosAtom, todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}
)
// components/TodoList.tsx
import { useAtom, useAtomValue } from 'jotai'
import { todosAtom, toggleTodoAtom } from '@/atoms/todo-atoms'
export function TodoList() {
const todos = useAtomValue(todosAtom)
const [, toggleTodo] = useAtom(toggleTodoAtom)
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
)
}
// components/TodoStats.tsx
export function TodoStats() {
// ✅ Derived state : se met à jour automatiquement
const stats = useAtomValue(todoStatsAtom)
return (
<div>
<p>Total: {stats.total}</p>
<p>Completed: {stats.completed}</p>
<p>Active: {stats.active}</p>
</div>
)
}
Avantages Jotai
- ✅ Atomic state : Fine-grained reactivity
- ✅ Derived state : Computed values élégants
- ✅ React Suspense : Async state natif
- ✅ TypeScript : Inference parfaite
- ✅ No provider hell : Pas de Context wrapping
- ✅ DevTools : Excellent support
Inconvénients
- ⚠️ Learning curve : Concept atomic moins intuitif
- ⚠️ Bundle size : 3.1 KB (vs 1.2 KB Zustand)
- ⚠️ SSR Next.js : Nécessite configuration
Recoil : Le Meta Choice
Philosophie
"Experimental state management library by Meta (Facebook)."
Créé par : Meta (Facebook)
Première version : 2020
GitHub stars : 20k+
Exemple Concret : Todo App
// recoil/todo-state.ts
import { atom, selector } from 'recoil'
type Todo = {
id: string
text: string
completed: boolean
}
export const todosState = atom<Todo[]>({
key: 'todos',
default: []
})
export const todoStatsState = selector({
key: 'todoStats',
get: ({ get }) => {
const todos = get(todosState)
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
}
}
})
// components/TodoList.tsx
import { useRecoilState, useRecoilValue } from 'recoil'
import { todosState } from '@/recoil/todo-state'
export function TodoList() {
const [todos, setTodos] = useRecoilState(todosState)
const toggleTodo = (id: string) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
)
}
// App.tsx : Requires RecoilRoot
import { RecoilRoot } from 'recoil'
export default function App() {
return (
<RecoilRoot>
<TodoList />
</RecoilRoot>
)
}
Avantages Recoil
- ✅ React Suspense : Async state natif
- ✅ DevTools : Time-travel debugging
- ✅ Selectors : Derived state puissant
- ✅ Meta backing : Utilisé chez Facebook
Inconvénients
- ❌ Bundle size : 79 KB (énorme vs alternatives)
- ⚠️ Experimental : Pas encore stable (2026!)
- ⚠️ SSR complexe : Nécessite RecoilRoot config
- ⚠️ Moins maintenu : Updates irrégulières
Benchmarks Performance
Test : 10 000 Todos Rendering
Setup :
- MacBook Pro M2 Max
- React 19
- 10 000 todos avec toggle simultané de 1000 items
| Librairie | Render Time | Memory Usage | Re-renders |
|---|---|---|---|
| Zustand | 142ms | 28 MB | 1 002 |
| Jotai | 138ms | 26 MB | 1 001 |
| Recoil | 156ms | 34 MB | 1 004 |
| Redux Toolkit | 189ms | 42 MB | 1 008 |
| Context API | 523ms | 58 MB | 10 002 |
Conclusion : Jotai légèrement plus rapide, Zustand excellent compromis, Context API catastrophique.
Migration depuis Redux
Zustand : Migration Facile
// ❌ AVANT : Redux Toolkit
import { createSlice, configureStore } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
},
})
const store = configureStore({
reducer: { counter: counterSlice.reducer },
})
// Usage
import { useDispatch, useSelector } from 'react-redux'
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()
dispatch(counterSlice.actions.increment())
// ✅ APRÈS : Zustand
import { create } from 'zustand'
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// Usage
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
increment() // Direct, pas de dispatch
Réduction de code : -60%
Patterns Avancés
Pattern 1 : Slices Zustand (Modulaire)
// store/slices/auth-slice.ts
type AuthSlice = {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
}
export const createAuthSlice: StateCreator<AuthSlice> = (set) => ({
user: null,
login: async (email, password) => {
const user = await api.login(email, password)
set({ user })
},
logout: () => set({ user: null }),
})
// store/slices/cart-slice.ts
type CartSlice = {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
}
export const createCartSlice: StateCreator<CartSlice> = (set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
})
// store/index.ts
export const useStore = create<AuthSlice & CartSlice>()((...a) => ({
...createAuthSlice(...a),
...createCartSlice(...a),
}))
Pattern 2 : Async Actions Jotai
// atoms/user-atom.ts
import { atom } from 'jotai'
// ✅ Async atom avec React Suspense
export const userAtom = atom(async () => {
const response = await fetch('/api/user')
return response.json()
})
// components/UserProfile.tsx
import { Suspense } from 'react'
import { useAtomValue } from 'jotai'
function UserProfileInner() {
// ✅ Suspense automatique pendant le fetch
const user = useAtomValue(userAtom)
return <div>{user.name}</div>
}
export function UserProfile() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfileInner />
</Suspense>
)
}
Recommandations par Projet
E-commerce (Next.js)
Recommandation : Zustand ✅
// store/cart-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useCartStore = create(
persist(
(set) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
total: () => /* compute */
}),
{ name: 'cart-storage' } // ✅ LocalStorage auto
)
)
Pourquoi : SSR Next.js facile, persist natif, simple
Dashboard Analytics (React SPA)
Recommandation : Jotai ✅
// atoms/analytics-atoms.ts
import { atom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
export const dateRangeAtom = atom('7d')
export const analyticsAtom = atomWithQuery((get) => ({
queryKey: ['analytics', get(dateRangeAtom)],
queryFn: async ({ queryKey: [, range] }) =>
fetch(`/api/analytics?range=${range}`).then((r) => r.json()),
}))
Pourquoi : Async state natif, derived state élégant
App Legacy (Migration)
Recommandation : Redux Toolkit ⚠️
Pourquoi : Si déjà en place, migration coûteuse. Gardez Redux mais updatez vers Redux Toolkit.
Checklist Choix
✅ Choisissez Zustand si :
- Projet nouveau ou petit/moyen
- Next.js SSR important
- Besoin persist (localStorage)
- Équipe junior/moyen
- Peu de state dérivé complexe
✅ Choisissez Jotai si :
- App complexe avec state dérivé
- React Suspense important
- Async state partout
- Équipe expérimentée React
- Performance critique
⚠️ Choisissez Recoil si :
- Déjà utilisé chez Meta/Facebook
- Besoin DevTools time-travel
- Bundle size pas important
❌ Évitez Redux si :
- Nouveau projet (over-engineered)
- Équipe < 10 devs
- App simple/moyenne
Conclusion : Zustand Domine 2026
État du marché state management React 2026 :
- Zustand : 🏆 Leader mainstream (47k stars)
- Jotai : 🚀 Montée rapide (18k stars)
- Recoil : ⚠️ Experimental forever
- Redux : 💀 Legacy (migration conseillée)
Chez Hulli Studio, nous utilisons Zustand par défaut (80% des projets) et Jotai pour projets complexes (20%).
Notre recommandation 2026 : Zustand pour 90% des use cases.
FAQ
Zustand fonctionne avec Next.js App Router ?
✅ Oui parfaitement. Utilisez create() côté client et Server Components pour data fetching.
Peut-on mixer Zustand + React Query ?
✅ Oui ! Pattern recommandé : Zustand pour UI state, React Query pour server state.
Migration Redux → Zustand combien de temps ?
10-20 jours pour app moyenne (50k lignes). Script automatisé disponible.
Articles connexes :