Zustand pour simplifier la gestion des états
Pourquoi utiliser Zustand ?
Dans le tutoriel sur le hook, nous avons vu comment utiliser UseContext
pour partager un état global.
En plus d'être un peu verbeux, utiliser useContext
peut devenir engendrer des difficiles à maintenir une applicationsi elle grandit, car tout est centralisé dans un seul contexte.
Il existe plusieurs librairies permettant de gérer un état global. La plus connue est Redux. Elle est utile pour de grosses applicaiton. Dans le cadre de ce cours nous avons décidé de vous montrer Zustand. Si vous êtes courieux, vous pouvez aller regarder d'autre librairies : Recoil ou Jotai.
Toutes ces libraires proposent de nombreuses fonctionalités. Ici nous nous concentrons sur le cas simple de partage d'une valeur et de méthodes, comme dans le tutoriel sur 'useContext'.
Pour comprendre Zustand, il est important de saisir le concept de store et comment il facilite la gestion d’état dans une application React.
Zustand signifie "état" en allemand. L'idée principale est de fournir un moyen centralisé, léger et performant pour gérer l’état global ou partagé d'une application React. Il s'inscrit dans le paradigme de la gestion d'état mais avec une simplicité qui contraste avec des outils comme Redux.
Un store est une entité où :
- L'état (les données) de l'application est conservé.
- Les actions (ou méthodes) permettent de modifier cet état.
- Tout composant peut y accéder directement sans avoir besoin de passage de props à travers plusieurs niveaux de composants.
Exemple simple
Dans le code ci-dessous :
useCounterStore.ts
définit le store (au sens deZustand
) c'est-à-dire les états qui seront partagés (pas dejsx
donc l'extension est.ts
)Compteur.tsx
consomme la variablecompteur
du storeCounterControls.tsx
consomme les 3 méthodes qui permettent de modifier le compteur (mais sans y accéder directement)
import { create } from 'zustand';
// Définir un type pour le store
type CounterState = {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
// Créer le store Zustand avec le type défini
const useCounterStore = create<CounterState>((set) => ({
count: 0, // État initial
increment: () => set((state) => ({ count: state.count + 1 })), // Méthode pour incrémenter
decrement: () => set((state) => ({ count: state.count - 1 })), // Méthode pour décrémenter
reset: () => set({ count: 0 }), // Méthode pour réinitialiser
}));
export default useCounterStore;
import React from 'react';
import useCounterStore from './useCounterStore';
const CounterDisplay: React.FC = () => {
const count = useCounterStore((state) => state.count); // Accéder à la valeur "count"
return <h1>Compteur : {count}</h1>;
};
export default CounterDisplay;
import React from 'react';
import useCounterStore from './useCounterStore';
const CounterControls: React.FC = () => {
const increment = useCounterStore((state) => state.increment); // Accéder à la méthode "increment"
const decrement = useCounterStore((state) => state.decrement); // Accéder à la méthode "decrement"
const reset = useCounterStore((state) => state.reset); // Accéder à la méthode "reset"
return (
<div>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
);
};
export default CounterControls;
Vous aurez remarqué que dans cet exemple la mécanique est très proche de UseContext
à deux différences près :
- Pas besoin d'englober les composants enfants dans une balise
<Provider>
. Il suffit de de faire un import - L'écriture du store est beaucoup moins verbeuse que celle du hook
UseContext
Je vous laisse comparer le code nécéssaire pour créer un composant thème clair/sombre avec Usecontext
versus Zustand
.
import React, { createContext, useContext, useState, ReactNode } from 'react';
type Theme = 'clair' | 'sombre';
type ThemeContextType = {
theme: Theme;
basculerTheme: () => void;
}
const ThemeContexte = createContext<ThemeContextType | undefined>(undefined);
type ThemeProviderProps = {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [theme, setTheme] = useState<Theme>('clair');
const basculerTheme = () => {
setTheme((prevTheme) => (prevTheme === 'clair' ? 'sombre' : 'clair'));
};
useEffect(() => {
document.body.className = theme === 'clair' ? 'theme-clair' : 'theme-sombre';
}, [theme]);
return (
<ThemeContexte.Provider value={{ theme, basculerTheme }}>
{children}
</ThemeContexte.Provider>
);
};
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContexte);
if (!context) {
throw new Error('useTheme doit être utilisé dans un ThemeProvider');
}
return context;
};
import { create } from 'zustand';
type Theme = 'clair' | 'sombre';
// Définir le type pour le store Zustand
type ThemeStore = {
theme: Theme;
basculerTheme: () => void;
};
// Créer le store Zustand
const useThemeStore = create<ThemeStore>((set) => ({
theme: 'clair', // Thème initial
basculerTheme: () =>
set((state) => ({
theme: state.theme === 'clair' ? 'sombre' : 'clair',
})),
}));
export default useThemeStore;
import React, { useEffect } from 'react';
import useThemeStore from './useThemeStore';
const ThemeSwitcher: React.FC = () => {
const { theme, basculerTheme } = useThemeStore((state) => ({
theme: state.theme,
basculerTheme: state.basculerTheme,
}));
// Appliquer une classe au body en fonction du thème
useEffect(() => {
document.body.className = theme === 'clair' ? 'theme-clair' : 'theme-sombre';
}, [theme]);
return (
<div>
<h1>Thème actuel : {theme}</h1>
<button onClick={basculerTheme}>Basculer le thème</button>
</div>
);
};
export default ThemeSwitcher;