Communitygithub.com

use-zustand

Utilisez ce skill pour gérer l'état Zustand. À associer avec `use-tanstack-query` pour séparer état client et serveur.

Qu'est-ce que use-zustand ?

Ce skill fournit des règles strictes et un guide avancé pour utiliser Zustand dans des projets React, en mettant l'accent sur une séparation claire entre l'état client (Zustand) et l'état serveur (TanStack Query). Il détaille comment organiser les stores avec le pattern slices pour éviter les gros blocs plats, comment typer proprement avec la forme curryfiée `create<State>()(...)`, et comment utiliser les middlewares `immer`, `persist` et `devtools` de manière appropriée. Le guide insiste sur des bonnes pratiques comme la sélection atomique avec `useShallow`, l'export d'actions nommées plutôt que l'appel direct à `set()`, et la non-duplication des valeurs dérivées. Il couvre également le débogage, les tests et le respect des contraintes Server Components (Next.js). Le skill est conçu pour être utilisé avec des assistants IA comme Claude Code, Cursor ou Codex, et s'applique à la rédaction et à la revue de stores Zustand dans des projets React/Next.js.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add C1131-G/skills

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Que fait use-zustand ?

Utilisez ce skill directement pour l'état Zustand. Associez-le à use-tanstack-query pour maintenir l'état client et l'état serveur dans leurs responsables respectifs.

Appliquez également enforce-code-quality et enforce-typescript-strict aux fichiers concernés.

Appliquez ces règles lors de l'écriture ou de la revue de stores Zustand dans un projet React.

1. Zustand est pour l'état client uniquement — ne chevauchez pas TanStack Query

Zustand gère l'état UI/client : sidebar ouverte/fermée, données de formulaire multi-étapes, thème, contenu du panier que l'utilisateur modifie activement. Les données serveur (tout ce qui est récupéré depuis une API) appartiennent à TanStack Query, ne les dupliquez pas dans un store Zustand — voir use-tanstack-query. Mélanger les deux responsabilités (par exemple copier des données récupérées dans Zustand « pour les rendre plus faciles d'accès ») crée deux sources de vérité qui peuvent se désynchroniser.

2. Pas de Provider nécessaire, mais client uniquement

Un store Zustand est un singleton au niveau du module — pas de wrapper contextuel requis, et il est accessible de n'importe où en important le hook. Mais cela signifie aussi qu'il dépend des API navigateur et d'un état par client, donc :

  • Initialisez/utilisez les stores Zustand uniquement dans les Client Components ('use client' dans Next.js).
  • Ne créez ni ne lisez jamais un store Zustand dans un Server Component — il n'y a pas d'isolation « par requête » comme avec un QueryClient frais, ce qui risque de fuiter l'état entre utilisateurs/requêtes.

3. Organisez les gros stores avec le pattern slices, pas un seul bloc plat

Un store unique avec des préoccupations non liées entassées (utilisateur, panier, thème UI, notifications, tout dans un seul objet) devient difficile à naviguer et à comprendre. Séparez par domaine en slices, puis composez-les en un seul appel create() :

// userSlice.ts
export interface UserSlice {
  user: User | null;
  setUser: (user: User) => void;
  logout: () => void;
}

export const createUserSlice: StateCreator<UserSlice> = (set) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null }),
});

// cartSlice.ts
export interface CartSlice {
  items: CartItem[];
  addItem: (item: CartItem) => void;
}

export const createCartSlice: StateCreator<CartSlice> = (set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
});

// store.ts
type AppState = UserSlice & CartSlice;

export const useAppStore = create<AppState>()((...a) => ({
  ...createUserSlice(...a),
  ...createCartSlice(...a),
}));

Utilisez des stores véritablement séparés (pas des slices) uniquement lorsque deux domaines sont totalement indépendants et n'ont jamais besoin d'interagir (par exemple un store d'authentification vs. un store d'analytique pur) — des stores multiples ajoutent une surcharge de coordination, donc privilégiez d'abord les slices au sein d'un seul store.

4. Utilisez des méthodes d'action nommées, pas d'appels set() bruts depuis les composants

Exposez les actions en tant que fonctions nommées sur le store ; n'appelez pas le set du store directement depuis un composant.

// À éviter : fonctionne mais pas sémantique — les composants doivent connaître la forme de l'état
const set = useAppStore((state) => state.set);
set({ theme: "dark" });

// Préféré : actions nommées, auto-documentées
const setTheme = useAppStore((state) => state.setTheme);
setTheme("dark");

Utilisez get() dans les actions d'une slice pour lire un autre état ou appeler les actions d'autres slices (par exemple une action logout qui vide aussi le panier) — mais méfiez-vous des dépendances circulaires entre slices qui s'appellent mutuellement.

5. Sélectionnez toujours la slice spécifique de l'état dont vous avez besoin — jamais le store entier

// À éviter : re-rendu sur TOUT changement du store, même sur des champs non liés
const state = useAppStore();

// Préféré : re-rendu uniquement quand `user` change
const user = useAppStore((state) => state.user);

6. Utilisez useShallow lors de la sélection de plusieurs valeurs ou d'un objet

Sélectionner un objet ou un tableau nouvellement construit depuis le store (par exemple { user, theme }) crée une nouvelle référence à chaque rendu même si les valeurs sous-jacentes n'ont pas changé, ce qui provoque des re-rendus inutiles. Enveloppez-le dans useShallow pour que Zustand compare les valeurs sélectionnées superficiellement au lieu de la référence :

import { useShallow } from "zustand/react/shallow";

const { user, theme } = useAppStore(
  useShallow((state) => ({ user: state.user, theme: state.theme }))
);

7. Utilisez le middleware immer pour les mises à jour d'état imbriquées

Spread manuel sur plusieurs niveaux d'état imbriqué pour mettre à jour un seul champ est sujet aux erreurs et verbeux. Le middleware immer permet d'écrire une syntaxe de « mutation » directe qui est en réalité appliquée de manière immuable sous le capot :

import { immer } from "zustand/middleware/immer";

const useStore = create<State>()(
  immer((set) => ({
    nested: { deeply: { value: 0 } },
    increment: () =>
      set((state) => {
        state.nested.deeply.value += 1; // semble mutable, est en réalité immuable
      }),
  }))
);

Évitez immer pour les mises à jour à très haute fréquence (par exemple état d'animation 60fps) où sa surcharge compte ; pour l'état de type CRUD typique, c'est un gain de lisibilité qui vaut le petit coût.

8. Typez les stores avec la forme curryfiée create<State>()(...)

Le create((set) => ({...})) simple ne peut pas être inféré de manière fiable par TypeScript pour un store plus complexe qu'un trivial, à cause de la façon dont le générique d'état est invariant. Utilisez toujours la forme curryfiée :

interface CounterState {
  count: number;
  increment: () => void;
}

const useCounterStore = create<CounterState>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}));

9. Ne stockez pas de valeurs dérivées/calculées — calculez-les dans un sélecteur ou le composant

Si une valeur peut être calculée à partir d'un autre état déjà dans le store (un total à partir d'une liste d'articles, un booléen à partir d'un champ statut), ne la stockez pas comme un champ séparé — elle se désynchronisera dès qu'un chemin de mise à jour oubliera de la recalculer. Calculez-la dans le sélecteur, ou comme une valeur dérivée simple dans le composant :

// À éviter : un champ `itemCount` séparé qui doit être maintenu manuellement à jour
addItem: (item) => set((state) => ({ items: [...state.items, item], itemCount: state.itemCount + 1 })),

// Préféré : dériver à la lecture
const itemCount = useCartStore((state) => state.items.length);

10. Utilisez persist pour l'état qui doit survivre entre sessions, devtools uniquement en développement

  • Utilisez le middleware persist (avec localStorage/sessionStorage, ou un adaptateur de stockage personnalisé) pour l'état qui doit véritablement survivre à un rechargement de page ou une nouvelle session (contenu du panier, préférences utilisateur) — pas pour l'état qui doit se réinitialiser à chaque visite.
  • Incluez une version et, si la forme change jamais, une fonction migrate dans les options de persist afin que l'état persistant ancien ne casse pas silencieusement l'application après un changement de schéma.
  • Utilisez le middleware devtools pour intégrer Redux DevTools et inspecter les changements d'état pendant le développement, mais assurez-vous qu'il n'est pas activé dans les builds de production.
export const useSettingsStore = create<SettingsState>()(
  devtools(
    persist(
      (set) => ({ theme: "light", setTheme: (theme) => set({ theme }) }),
      { name: "settings-storage", version: 1 }
    )
  )
);

11. Utilisez devtools pour tracer le flux d'état et associez-le à React DevTools pour détecter les re-rendus inutiles

devtools n'est pas qu'un simple logger pratique — utilisez-le activement pour déboguer pourquoi l'état a changé, pas seulement que ça a changé :

  • Nommez chaque action pour que la timeline des devtools montre une trace significative au lieu d'une entrée set anonyme pour chaque mise à jour. Passez un troisième argument à set avec un nom/type d'action :

    addItem: (item) =>
      set(
        (state) => ({ items: [...state.items, item] }),
        false,
        "cart/addItem" // apparaît comme ce label dans la liste d'actions de Redux DevTools
      ),
    
  • Nommez chaque store via devtools(..., { name: "cart-store" }) quand un projet a plusieurs stores, pour que le panneau des devtools n'affiche pas un mélange ambigu de mises à jour de tous les stores à la fois.

  • Utilisez la timeline des devtools pour confirmer l'ordre réel du flux d'état lorsqu'un bug ressemble à une condition de course ou à un problème d'ordre de mise à jour — parcourez la liste d'actions pour voir exactement ce qui a changé, quand, et avec quel état précédent/suivant, plutôt que de deviner à partir de console.logs dispersés dans le code des actions.

  • Le diagnostic des re-rendus inutiles est un outil séparé, pas quelque chose que devtools montre lui-même. Utilisez le Profiler de React DevTools (ou why-did-you-render en développement si vous avez besoin de détails au niveau du site d'appel) pour voir quels composants se sont re-rendus suite à un changement d'état donné et pourquoi. Si un composant se re-rend suite à une mise à jour du store qui n'a pas touché les champs qu'il sélectionne, c'est le signe qu'il manque un sélecteur (règle 5) ou un enrobage useShallow (règle 6) — allez corriger le sélecteur, pas le store.

12. Rendez les stores testables : exportez le créateur brut, pas seulement le hook lié

Exportez à la fois le hook (pour les composants) et le StateCreator/la fonction de création de store sous-jacente (pour les tests), afin que les tests puissent créer une instance de store fraîche et isolée par test au lieu de partager le singleton au niveau du module sur toute la suite de tests (ce qui fait fuiter l'état entre tests).

Application de ces règles

  • Nouveau store : commencez avec un seul store utilisant le pattern slices dès le premier jour si plus d'un domaine est impliqué ; typez-le avec la forme curryfiée create<State>()(...) ; ajoutez persist/devtools uniquement si l'état en a réellement besoin.
  • Revue de code existant : signalez la sélection du store entier sans fonction de sélecteur, les appels set() bruts depuis les composants au lieu d'actions nommées, l'état dérivé stocké comme champ séparé, les données récupérées du serveur dupliquées dans Zustand au lieu de TanStack Query, tout store touché depuis un Server Component, les actions/stores non nommés qui rendent la timeline des devtools difficile à lire, et tout composant qui se re-rend suite à des changements non liés du store (sélecteur manquant ou useShallow).

Skills associés