State Management Minimaliste pour React
Simple. Puissant. Sans Provider.
1. Comprendre le state global
Prop drilling et limites de Context
2. Créer un store Zustand
create(), state + actions
3. Optimiser avec les selectors
Éviter les re-renders inutiles
4. Configurer les middlewares
persist, devtools, immer
5. Organiser en slices
Pour les grosses applications
Le problème du prop drilling
Et les limites de Context pour le state global
Pourquoi Zustand ?
Comparaison avec Redux, Context, Jotai
Créer un store avec create()
State + actions dans un seul objet
Selectors et optimisation
Sélectionner précisément ce dont on a besoin
Live coding : Panier e-commerce
Store complet avec middlewares
Passer des props Ă travers plusieurs niveaux de composants
Re-renders en cascade
Tout composant consommant le Context re-render Ă chaque changement
Boilerplate
Provider, Consumer, createContext... Beaucoup de code
Pas de sélecteurs
Impossible de sélectionner une partie du state
Provider obligatoire
Doit envelopper toute l'application
Context est parfait pour le state local, mais pas pour le state global
Une solution minimaliste et élégante
"Bear state" en allemand — simple et robuste
📦
1.3 KB
Gzipped
đźš«
0 Provider
Requis
⚡
Hooks natifs
React-friendly
| Critère | Redux | Context | Zustand | Jotai |
|---|---|---|---|---|
| Taille | ~7 KB | 0 KB | ~1.3 KB | ~2 KB |
| Provider requis | ✅ Oui | ✅ Oui | ❌ Non | ✅ Oui |
| Boilerplate | Beaucoup | Moyen | Minimal | Minimal |
| Selectors | ✅ | ❌ | ✅ | ✅ |
| Learning curve | Élevée | Basse | Très basse | Moyenne |
Zustand : le meilleur équilibre simplicité/performance
Pas de Provider nécessaire
Le store est accessible directement depuis n'importe quel composant
API ultra-simple
Un seul import : create
Hooks natifs
Utilisable comme un hook React standard
Middlewares optionnels
persist, devtools, immer... au choix
# npm
npm install zustand
# yarn
yarn add zustand
# pnpm
pnpm add zustand
C'est tout! Pas de configuration supplémentaire nécessaire.
create() — Le cœur de ZustandCréer un store en une seule fonction
State + Actions = Store
Tout dans un seul objet
import { create } from 'zustand'
const useBearStore = create((set, get) => ({
// State
bears: 0,
// Actions
increase: (by) => set((state) => ({
bears: state.bears + by
})),
reset: () => set({ bears: 0 }),
}))
set()
Met Ă jour le state (shallow merge)
get()
Lit le state actuel
store.js
import { create } from 'zustand'
export const useCounter = create((set) => ({
count: 0,
increment: () => set((s) => ({
count: s.count + 1
})),
decrement: () => set((s) => ({
count: s.count - 1
})),
reset: () => set({ count: 0 }),
}))
Counter.jsx
import { useCounter } from './store'
function Counter() {
const { count, increment, reset }
= useCounter()
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>
+1
</button>
</div>
)
}
Pas de Provider! Le store est utilisable directement.
set(newState)
Objet direct : set({ count: 5 })
Fonction : set(s => ({ count: s.count + 1 }))
get()
Retourne le state actuel
Utile dans les actions async
const current = get()
set() fait un shallow merge par défaut
Les propriétés non mentionnées sont préservées
✅ Shallow merge (défaut)
// State: { a: 1, b: 2 }
set({ a: 10 })
// Résultat: { a: 10, b: 2 }
b est préservé!
❌ Remplacement total
// State: { a: 1, b: 2 }
set({ a: 10 }, true)
// Résultat: { a: 10 }
b est perdu!
Le 2ème paramètre true force le remplacement total
Sélectionner tout le store = re-render à chaque changement
const store = useBearStore()
❌ Re-render si N'IMPORTE quelle propriété change!
❌ Mauvais
const store = useBearStore()
const bears = store.bears
Re-render si bears OU fish OU honey changent
âś… Bon
const bears = useBearStore(
state => state.bears
)
Re-render SEULEMENT si bears change
Le selector est une fonction qui retourne la valeur souhaitée
Quand on a besoin de plusieurs propriétés sans re-renders inutiles
import { useShallow } from 'zustand/react/shallow'
const { bears, honey } = useBearStore(
useShallow((state) => ({
bears: state.bears,
honey: state.honey
}))
)
useShallow compare les valeurs avec shallow equality
Re-render seulement si l'une des valeurs change
âś… Callback simple suffit
Valeur unique (primitif)
const count = useStore(s => s.count)
Fonction unique (stable)
const addItem = useStore(s => s.addItem)
Les actions sont des références stables
⚠️ useShallow nécessaire
Plusieurs valeurs (objet)
const { a, b } = useStore(
useShallow(s => ({ a: s.a, b: s.b }))
)
Sans useShallow: nouvel objet → re-render!
Plusieurs actions
const { add, remove } = useStore(
useShallow(s => ({ add: s.add, remove: s.remove }))
)
Règle : Objet/Array retourné → useShallow | Valeur unique → callback simple
Avant — 3 re-renders
function BearInfo() {
const store = useBearStore()
return <p>{store.bears}</p>
}
Re-render si bears, fish OU honey changent
Après — 1 re-render
function BearInfo() {
const bears = useBearStore(
s => s.bears
)
return <p>{bears}</p>
}
Re-render SEULEMENT si bears change
3x moins de re-renders = meilleure performance!
Étendre les fonctionnalités du store
persist
localStorage/sessionStorage
devtools
Redux DevTools
immer
Mutations immutables
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({
count: 0,
increment: () => set((s) => ({
count: s.count + 1
})),
}),
{ name: 'my-storage' }
)
)
Le state est automatiquement sauvegardé dans localStorage
persist(storeFn, {
// Nom de la clé dans localStorage
name: 'cart-storage',
// Choisir le storage (défaut: localStorage)
storage: createJSONStorage(() => sessionStorage),
// Sélectionner les champs à persister
partialize: (state) => ({
items: state.items, // pas le total
}),
})
partialize permet de ne pas sauvegarder les données calculées
import { devtools } from 'zustand/middleware'
const useStore = create(
devtools(
(set) => ({
count: 0,
increment: () => set({ count: 1 }),
}),
{ name: 'MyStore' }
)
)
Installez Redux DevTools Extension pour debugger votre store
Time travel, actions history, state inspection
Modifier le state comme s'il était mutable
Sans immer
set((s) => ({
user: {
...s.user,
name: 'John'
}
}))
Avec immer
set((s) => {
s.user.name = 'John'
})
immer gère l'immutabilité pour vous — plus lisible!
L'ordre compte : de l'intérieur vers l'extérieur
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'
const useStore = create()(
devtools(
persist(
immer(
(set) => ({
count: 0,
increment: () => set((s) => { s.count++ }),
}),
),
{ name: 'store' }
),
{ name: 'MyStore' }
)
)
Ordre typique : devtools > persist > immer > store
Organiser les gros stores en modules
Comme Redux, mais sans le boilerplate
bearSlice.js
const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((s) => ({
bears: s.bears + 1
})),
})
fishSlice.js
const createFishSlice = (set) => ({
fishes: 0,
addFish: () => set((s) => ({
fishes: s.fishes + 1
})),
})
Chaque slice est une fonction qui reçoit set et retourne un objet
import { createBearSlice } from './bearSlice'
import { createFishSlice } from './fishSlice'
export const useBoundStore = create((...a) => ({
...createBearSlice(...a),
...createFishSlice(...a),
}))
Le spread ... combine tous les slices en un seul store
Store Panier E-commerce avec Zustand
Features : Ajouter, Supprimer, Vider, Persister
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useCartStore = create(
persist(
(set, get) => ({
// State
items: [],
total: 0,
// Actions
addItem: (product) => set((s) => ({
items: [...s.items, product],
total: s.total + product.price
})),
removeItem: (id) => set((s) => {
const item = s.items.find(i => i.id === id)
return {
items: s.items.filter(i => i.id !== id),
total: s.total - item.price
}
}),
clearCart: () => set({ items: [], total: 0 }),
}),
{ name: 'cart-storage' }
)
)
import { useCartStore } from './cartStore'
import { useShallow } from 'zustand/react/shallow'
function Cart() {
// Selectors optimisés
const items = useCartStore(s => s.items)
const total = useCartStore(s => s.total)
const { addItem, removeItem, clearCart } = useCartStore(
useShallow(s => ({
addItem: s.addItem,
removeItem: s.removeItem,
clearCart: s.clearCart
}))
)
return (
<div>
<h2>Panier ({items.length})</h2>
<p>Total: {total}€</p>
<button onClick={clearCart}>Vider</button>
</div>
)
}
Chaque selector est indépendant — re-renders minimaux!
❌ Sélectionner tout
const s = useStore()
Au lieu de :
const x = useStore(s => s.x)
⚠️ Shallow merge oublié
set() fait un MERGE, pas un remplacement
Les propriétés non mentionnées sont préservées
⚠️ Async dans set()
Ne pas mettre de logique async dans set()
La séparer dans une action :
fetchData: async () => {
const data = await api()
set({ data })
}
create()
Selectors
Middlewares
Points clés
Zustand : Simple. Puissant. Sans Provider.