
Zustand : Approche minimaliste basée sur les hooks
Découvrez Zustand, une bibliothèque de gestion d'état minimaliste pour React, basée sur les hooks, offrant simplicité, flexibilité et moins de boilerplate que Redux.
Une alternative légère et flexible à redux
Alors que Redux (même avec Redux Toolkit) offre une solution robuste et structurée pour la gestion d'état globale, sa courbe d'apprentissage et son architecture peuvent parfois sembler excessives pour des projets moins complexes ou pour des équipes préférant une approche moins dogmatique. Zustand (prononcé "Tsou-chtant", mot allemand signifiant "état") est une bibliothèque de gestion d'état qui a gagné en popularité en proposant une alternative significativement plus simple, plus flexible et avec beaucoup moins de code répétitif (boilerplate).
La philosophie de Zustand repose sur la simplicité et l'utilisation directe des hooks React. Elle ne nécessite pas d'envelopper l'application dans des composants `
Créer un store zustand avec `create`
Le coeur de Zustand est la fonction `create`. Elle prend en argument une fonction "creator" qui reçoit une fonction `set` (et optionnellement `get` et `api`) et doit retourner l'objet initial du store. Cet objet contient à la fois les propriétés de l'état et les fonctions (actions) qui peuvent le modifier.
Contrairement à Redux qui sépare strictement l'état (dans les reducers) et les actions (objets dispatchés), Zustand regroupe l'état et les fonctions qui le modifient au sein du même objet store, ce qui peut sembler plus intuitif pour de nombreux développeurs.
// store/compteurStore.js
import { create } from 'zustand'; // Importer la fonction create
// La fonction 'creator' prend 'set' en argument
const useCompteurStore = create((set) => ({
// Partie état du store
compteur: 0,
utilisateur: null,
// Partie actions (fonctions qui modifient l'état via 'set')
increment: () => set((state) => ({ compteur: state.compteur + 1 })),
// 'set' reçoit l'état actuel et retourne un objet partiel à fusionner
decrement: () => set((state) => ({ compteur: state.compteur - 1 })),
incrementByAmount: (amount) => set((state) => ({ compteur: state.compteur + amount })),
setUser: (user) => set({ utilisateur: user }), // Peut aussi retourner directement l'objet partiel
reset: () => set({ compteur: 0 }), // Remplace/fusionne uniquement 'compteur'
}));
export default useCompteurStore;Ici, `create` retourne un hook personnalisé (`useCompteurStore`) que nous pourrons utiliser dans nos composants React. Le store contient l'état (`compteur`, `utilisateur`) et les actions (`increment`, `decrement`, etc.).
Utiliser le store dans les composants react
L'utilisation du store Zustand dans un composant React est remarquablement simple. Il suffit d'importer le hook généré (`useCompteurStore` dans notre exemple) et de l'appeler.
Accéder à l'état complet : Appeler le hook sans argument retourne l'intégralité de l'objet store (état + actions). Attention, cela entraînera un re-rendu du composant à chaque modification du store, même mineure.const store = useCompteurStore(); // store.compteur, store.increment()Accéder à une partie de l'état (Sélection) : Pour optimiser les performances et ne re-rendre le composant que lorsque la donnée spécifique dont il a besoin change, il est fortement recommandé d'utiliser un sélecteur. Passez une fonction en argument du hook qui prend l'état complet et retourne uniquement la valeur souhaitée. Zustand comparera l'ancienne et la nouvelle valeur retournée par le sélecteur et ne déclenchera un re-rendu que si elle a changé (par défaut, comparaison de référence stricte `===`).import React from 'react';
import useCompteurStore from '../store/compteurStore';
function AfficheurCompteurZustand() {
// Sélectionne uniquement la valeur du compteur
const compteur = useCompteurStore((state) => state.compteur);
return Compteur (Zustand) : {compteur}
;
// Ce composant ne re-rendra que si state.compteur change
}
function BoutonsCompteurZustand() {
// Sélectionne les actions nécessaires (ne causent pas de re-rendu si elles ne changent pas)
const increment = useCompteurStore((state) => state.increment);
const decrement = useCompteurStore((state) => state.decrement);
const reset = useCompteurStore((state) => state.reset);
// Alternative : sélectionner plusieurs valeurs (attention à la mémoïsation si on retourne un objet)
// const { increment, decrement } = useCompteurStore(state => ({ increment: state.increment, decrement: state.decrement }), shallow); // shallow de zustand/shallow
return (
);
}
export { AfficheurCompteurZustand, BoutonsCompteurZustand };Pour la sélection de plusieurs valeurs ou d'objets non primitifs, il faut utiliser une fonction de comparaison personnalisée (comme `shallow` fournie par `zustand/shallow`) pour éviter des re-rendus inutiles dus à la création de nouveaux objets à chaque rendu.
Mettre à jour l'état avec `set`
La fonction `set`, fournie à la fonction `creator` de `create`, est le moyen de modifier l'état du store. Par défaut, lorsque vous appelez `set` avec un objet, cet objet est fusionné (merged) avec l'état existant, de manière similaire au `this.setState` des anciens composants classe React. Seules les clés fournies dans l'objet sont mises à jour ; les autres restent inchangées.
// Dans le creator du store
increment: () => set((state) => ({ compteur: state.compteur + 1 })),
// Fusionne { compteur: ... } avec l'état existant ({ utilisateur: ... } reste inchangé)
resetCompteur: () => set({ compteur: 0 }),
// Fusionne { compteur: 0 } avec l'état existant
Si vous avez besoin de remplacer complètement l'état ou d'effectuer une mise à jour basée sur l'état précédent de manière plus complexe, vous pouvez passer une fonction à `set`. Si vous voulez explicitement remplacer l'état au lieu de le fusionner, vous pouvez le faire en passant un deuxième argument `true` à `set`, mais c'est moins courant : `set(newState, true) // Remplace l'état complet`.
Comme pour Redux Toolkit, Zustand peut être configuré pour utiliser Immer (via un middleware), ce qui permet d'écrire des mises à jour d'état avec une syntaxe "mutable" :
// Avec le middleware immer
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
const useStore = create(immer((set) => ({
compteur: 0,
increment: () => set((state) => { state.compteur += 1; }), // Syntaxe mutable grâce à Immer
})));Gérer l'asynchronisme simplement
Un avantage notable de Zustand est sa gestion simple des actions asynchrones. Il n'y a pas besoin de middleware comme Thunk ou Saga par défaut. Vous pouvez simplement rendre vos fonctions d'action `async` et utiliser `await` à l'intérieur. Appelez `set` avant et après l'opération asynchrone pour gérer les états de chargement, de succès ou d'erreur.
// store/userStore.js
import { create } from 'zustand';
const useUserStore = create((set) => ({
userData: null,
loading: false,
error: null,
fetchUserData: async (userId) => {
set({ loading: true, error: null }); // Début du chargement
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Utilisateur non trouvé');
const data = await response.json();
set({ userData: data, loading: false }); // Succès
} catch (error) {
set({ error: error.message, loading: false }); // Erreur
}
},
}));
export default useUserStore;Extensibilité via les middlewares
Bien que minimaliste, Zustand est extensible grâce à un système de middlewares. Vous pouvez ajouter des fonctionnalités supplémentaires au store lors de sa création. Certains middlewares courants incluent :
- `devtools` : Pour connecter votre store aux Redux DevTools, permettant l'inspection de l'état et le time-travel debugging.
- `persist` : Pour sauvegarder automatiquement l'état du store (ou une partie) dans le `localStorage` ou `sessionStorage` et le restaurer au chargement de l'application.
- `immer` : Pour activer la syntaxe de mise à jour "mutable" mentionnée précédemment.
Ces middlewares s'appliquent en enveloppant la fonction `creator` lors de l'appel à `create`.
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useAuthStore = create(devtools(persist((set) => ({
token: null,
user: null,
setToken: (token) => set({ token }),
// ... autres actions
}), {
name: 'auth-storage', // Nom de la clé dans localStorage
// getStorage: () => sessionStorage, // Optionnel : utiliser sessionStorage
})));Conclusion : Simplicité et puissance à la demande
Zustand se présente comme une excellente alternative pour la gestion d'état globale dans React, particulièrement appréciée pour :
- Sa simplicité et son faible boilerplate : Moins de code à écrire comparé à Redux classique ou même RTK pour des cas simples.
- Sa flexibilité : Moins dogmatique que Redux, il regroupe état et actions et gère l'asynchrone nativement.
- Son approche basée sur les hooks : Intégration naturelle dans l'écosystème React moderne.
- Ses performances : Le système de sélection granulaire permet d'optimiser les re-rendus.
- Son extensibilité : Les middlewares permettent d'ajouter des fonctionnalités avancées au besoin.
C'est un choix judicieux pour les projets où Redux semble surdimensionné, mais où l'API Context commence à montrer ses limites, ou simplement pour les équipes qui préfèrent une approche plus directe et moins structurée de la gestion d'état globale.