Un des problèmes que l'on rencontre rapidement lorsque l'on crée une application React est le partage d'un état entre plusieurs composants qui n'ont pas un parent proche. Pour éviter de faire du props drilling il est possible d'utiliser les contextes mais leur utilisation peut rapidement s'avérer complexe. Zustand offre une solution alternative de gestion d'état partagé avec une approche plus simple.
Pour créer un store (état partagé), on va utiliser la méthode create
import { create } from 'zustand'
export const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
Ce store peut ensuite être utilisé dans un composant à l'aide du hook généré par la fonction create
.
export function App () {
const {bears, increasePopulation} = useBearStore()
return <div>
<p>Il y a {bears} ours.</p>
<button onClick={increasePopulation}>Incrémenter</button>
</div>
}
Il est aussi possible de définir un sélecteur pour extraire un élément spécifique de notre store.
export function App () {
const bears = useBearStore(state => state.bears)
return <div>
<p>Il y a {bears} ours.</p>
</div>
}
Le sélecteur permet de ne récupérer qu'une partie du store et permet aussi d'optimiser les re-rendu. En effet, le composant ne sera rendu que lorsque la valeur retourné par le sélecteur change.
Actions hors du store
Dans l'exemple ci-dessus nous avons mis les méthodes d'incrémentations / décrémentations dans l'état mais il est possible de créer les méthodes en dehors de l'état.
import { create } from 'zustand'
export const useBearStore = create((set) => ({
bears: 0,
}))
export function increasePopulation () {
useBearStore.setState(state => ({bears: state.bears + 1}))
}
Cette approche permet d'avoir des méthodes globales qui peuvent être consommées par les composants sans nécessiter un hook.
Mise à jour de l'état
Lorsque l'on met à jour l'état il y a plusieurs choses que l'on peut noter :
- Les mutations doivent être immutables (même règle que pour un setState) et ne pas modifier l'objet original.
- L'objet renvoyé dans la fonction de mise à jour
set
oustore.setState
sera fusionné avec l'état actuel (la fusion ne se fait pas en profondeur mais que pour les éléments de premier niveau).
Pour des modifications en profondeur, et pour éviter d'avoir trop de code à écrire, il est possible d'utiliser immer.
import {produce} from 'immer'
{
// Sans immer on est obligé de restructuré l'objet
// Plus il y a de profondeur plus c'est complexe
updateUsername: (username) => set(state => ({
user: {...state.user, username: state.user.username}
}))
// Avec Immer on peut écrire une mutation, tout en conservant l'immutabilité
updateUsernameImmer: (username) => set(produce(state => {
state.user.username = username
}))
}
Middleware
Zustand dispose aussi d'un système de middleware qui permet d'ajouter de la logique lorsqu'un store est modifié. Par exemple, le middleware persist
permet de stocker les informations du store en mémoire (localstorage) pour le réutiliser.
import { create } from 'zustand'
export const useBearStore = create(
persist(
(set, get) => ({
bears: 0,
addABear: () => set({ bears: get().bears + 1 }),
}),
{
name: 'bear-storage',
},
),
)
Il existe d'autres middleware comme par exemple devtool qui permet de pouvoir utiliser le redux devtool avec le store zustand.
Intégration avec TypeScript
Zustand peut être utilisé avec TypeScript mais cela nécessite quelques adaptations au niveau de la syntaxe.
import { create } from 'zustand'
import { combine } from 'zustand/middleware'
const useBearStore = create(
combine({
// On définit l'état ici
bears: 0
}, (set) => ({
// Les méthodes seront définit dans un second temps
increase: (by: number) => set((state) => ({ bears: state.bears + by })),
})),
)
L'utilisation du combine
permet au type d'être inféré automatiquement pour ne pas avoir à le créer en amont.