Codelynx.dev
Posts

Ce dont personne ne parle à propos de Zustand

16/11/2024 Melvynx

Zustand est depuis quelques années une librairie qui fait pour moi "partie de React."

C'est-à-dire que...

La méthode qu'il expose (create) est devenu essentiel dans presque toutes mes applications.

La magie de Zustand c'est qu'il vient résoudre un gros problème, la gestion de state globaux, de manière si simple et efficace que c'est déstabilisant.

On va parler de Zustand et pourquoi tu as besoin de cette outil.

C'est quoi Zustand ?

C'est une librairie pour gérer des stores dans React !

Un Store permet de stocker des données et des actions qui seront accessibles partout dans ton application.

Par exemple tu pourrais vouloir stocker un panier :

JS
const useCart = create((set, get) => ({
  // Notre état
  items: [],
  // Une méthode stable qui vient ajouter un item
  addItem: (item) => set({ items: [...get().items, item] }),
  // Une méthode stable qui vient supprimer un item
  removeItem: (item) => set({ items: get().items.filter((i) => i !== item) }),
}));

Contrairement à un context, ce store sera accessible n'importe où dans ton application :

JS
import { useCart } from './useCart';

export default function App() {
  const { items, addItem, removeItem } = useCart();

  return (
    <div>
      <button onClick={() => addItem('item1')}>Add item</button>
      <button onClick={() => removeItem('item1')}>Remove item</button>
      <ul>
        {items.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

En règle générale, si tu veux un state global ce que tu veux c'est Zustand et jamais useContext.

Pourquoi Zustand ?

Il apporte pas mal d'optimisations notamment le fait de pouvoir "sélectionner" des données :

JS
const addItem = useCart((state) => state.addItem);

Quand tu fais ce code, tu viens t'abonner à l'évènement addItem et ton component ne se re-render que lorsque addItem change (c'est-à-dire jamais car c'est une méthode).

Il possède des hooks d'optimisation comme useShallow etc... qui permettent d'éviter des re-renders etc...

En règle générale useContext n'est pas le bon outil pour gérer des states globaux.

Quelle est la feature dont personne ne parle ?

La magie de Zustand c'est de pouvoir interagir avec React en dehors de React.

C'est-à-dire que tu peux modifier un store en dehors d'un component.

Un des components que j'aime beaucoup sur Codeline c'est un component qui affiche des AlertDialog :

JS
const useAlertDialog = create((set, get) => ({
  dialogs: [],
  addDialog: (dialog) => set({ dialogs: [...get().dialogs, dialog] }),
  removeDialog: (dialog) =>
    set({ dialogs: get().dialogs.filter((d) => d !== dialog) }),
}));

const AlertDialogs = () => {
  const dialogs = useAlertDialog((state) => state.dialogs);

  const dialogToShow = dialogs[0];

  if (!dialogToShow) return null;

  return <AlertDialog {...dialogToShow} />;
};

En gros ce component me permet d'afficher des Alert dans mon application React sans avoir besoin de venir "l'afficher" partout.

Par exemple :

  • Quand l'utilisateur va faire une action destructive, j'affiche une alert qui lui demande de confirmer

Le truc c'est que pour l'utiliser avec un React tu dois faire ça :

JS
const Demo = () => {
  // Ajouter une première ligne pour pouvoir utiliser useAlertDialog
  const addDialog = useAlertDialog((state) => state.addDialog);

  const handleClick = () => {
    // Appeler notre méthode de store
    addDialog({
      title: 'Demo',
      description: 'Demo',
      action: {
        label: 'Confirm',
        onClick: () => {
          console.log('Confirm');
        },
      },
    });
  };

  return <button onClick={handleClick}>Click me</button>;
};

L'avantage de Zustand c'est justement de pouvoir utiliser notre Store dans des simples méthodes.

Ce que j'ai fait c'est ça :

JS
export const alertDialog = {
  add: (params: ConfirmationDialogProps) => {
    useModalStore.getState().enqueueConfirmationModal(params);
  },
  // autre méthode...
};

De cette manière je peux utiliser alertDialog dans mon application React sans avoir besoin d'importer le hook etc...

JS
const Demo = () => {
  const handleClick = () => {
    // Je peux directement utiliser alertDialog
    alertDialog.add({
      title: 'Demo',
      description: 'Demo',
      action: {
        label: 'Confirm',
        onClick: () => {
          console.log('Confirm');
        },
      },
    });
  };

  return <button onClick={handleClick}>Click me</button>;
};

Quand je fais ça, je viens "en dehors de React" sans utiliser de hook interagir avec mon store et afficher une Dialog.

C'est ça la feature assez géniale de Zustand.

Voici à quoi ça ressemble 2 :

import React from "react";
import { create } from "zustand";
import clsx from "clsx";

const useModalStore = create((set) => ({
  modals: [],
  enqueueConfirmationModal: (modal) =>
    set((state) => ({ modals: [...state.modals, modal] })),
  dequeueConfirmationModal: () =>
    set((state) => ({ modals: state.modals.slice(1) })),
}));

export const alertDialog = {
  add: (params) => {
    useModalStore.getState().enqueueConfirmationModal(params);
  },
  remove: () => {
    useModalStore.getState().dequeueConfirmationModal();
  },
};

const AlertDialog = ({ title, description, action }) => (
  <div className="bg-opacity-50 fixed inset-0 flex items-center justify-center bg-black">
    <div className="rounded bg-white p-4 shadow-lg">
      <h2 className="text-lg font-bold">{title}</h2>
      <p className="mt-2">{description}</p>
      <button
        className="mt-4 rounded bg-blue-500 px-4 py-2 text-white"
        onClick={() => {
          action.onClick();
          alertDialog.remove();
        }}
      >
        {action.label}
      </button>
    </div>
  </div>
);

const Demo = () => {
  const handleClick = () => {
    alertDialog.add({
      title: "Demo",
      description: "Demo",
      action: {
        label: "Confirm",
        onClick: () => {
          console.log("Confirm");
        },
      },
    });
  };

  const modals = useModalStore((state) => state.modals);

  return (
    <div className="flex flex-col items-center justify-center h-screen bg-gray-100">
      <button
        onClick={handleClick}
        className="bg-blue-500 text-white px-4 py-2 rounded"
      >
        Click me
      </button>
      {modals.map((modal, index) => (
        <AlertDialog key={index} {...modal} />
      ))}
    </div>
  );
};

export default Demo;

Mais attends, tu es sûr que c'est bien ?

Attention, il faut savoir que les hooks sont là pour une raison.

Il ne faudrait en général pas récupérer une donnée sans un hook car ton component ne sera pas "abonné" au changement de celle-ci.

Mais pour des événements, c'est totalement ok car Zustand fonctionne d'une manière où toutes les méthodes ne sont jamais recréées.

Dans ma formation React il y a un module complet sur Zustand car je pense sincèrement qu'il est essentiel de comprendre cette librairie.

Reçois une formation React gratuite
Deviens un expert React en comprenant tous les concepts avancés de cette librairie.

Dans le même style il est intéressant pour toi de comprendre la différence entre NextJS et React ou la différence entre React et VueJS ou même tout simplement savoir ce qu'es vertiablement React.