Zustand

State Management Minimaliste pour React

Simple. Puissant. Sans Provider.

Objectifs de la leçon

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

Plan du cours

1

Le problème du prop drilling

Et les limites de Context pour le state global

2

Pourquoi Zustand ?

Comparaison avec Redux, Context, Jotai

3

Créer un store avec create()

State + actions dans un seul objet

4

Selectors et optimisation

Sélectionner précisément ce dont on a besoin

5

Live coding : Panier e-commerce

Store complet avec middlewares

Le problème du Prop Drilling

Passer des props Ă  travers plusieurs niveaux de composants

App user
↓
Layout user
↓
Sidebar user
↓
Profile user
↓
Avatar utilise user!

Les limites de Context

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

Pourquoi Zustand ?

Une solution minimaliste et élégante

"Bear state" en allemand — simple et robuste

📦

1.3 KB

Gzipped

đźš«

0 Provider

Requis

⚡

Hooks natifs

React-friendly

Comparaison des solutions

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

Avantages clés de Zustand

đźš«

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

Installation

# 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 Zustand

Créer un store en une seule fonction

State + Actions = Store

Tout dans un seul objet

State + Actions 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

Exemple : Store Counter

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() et get() en détail

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 vs Remplacement

✅ 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

Le problème des Re-renders

Sélectionner tout le store = re-render à chaque changement

const store = useBearStore()

❌ Re-render si N'IMPORTE quelle propriété change!

Selectors : Sélectionner précisément

❌ 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

useShallow : Multiple valeurs

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

Quand utiliser useShallow vs callback simple ?

âś… 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 / Après optimisation

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!

Middlewares

Étendre les fonctionnalités du store

persist

localStorage/sessionStorage

devtools

Redux DevTools

immer

Mutations immutables

persist — Sauvegarde automatique

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

Options de persist

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

devtools — Redux DevTools

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

immer — Mutations immutables

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!

Combiner les middlewares

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

Slices Pattern

Organiser les gros stores en modules

Comme Redux, mais sans le boilerplate

Exemple : BearSlice + FishSlice

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

Combiner les slices

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

Live Coding

Store Panier E-commerce avec Zustand

Features : Ajouter, Supprimer, Vider, Persister

cartStore.js

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' }

)

)

Cart.jsx — Utilisation optimisée

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!

Pièges courants

❌ 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 })

}

Récapitulatif

create()

  • • State + Actions dans un objet
  • • set() pour mettre Ă  jour
  • • get() pour lire le state

Selectors

  • • Toujours utiliser un selector
  • • useShallow pour plusieurs valeurs
  • • Évite les re-renders inutiles

Middlewares

  • • persist : localStorage
  • • devtools : debugging
  • • immer : mutations immutables

Points clés

  • • Pas de Provider nĂ©cessaire
  • • Shallow merge par dĂ©faut
  • • Slices pour gros stores

Zustand : Simple. Puissant. Sans Provider.