Gestion d'état React avec Claude Code : guide pratique
Organisez l'état React avec Claude Code : Context, Zustand, Jotai, TanStack Query, tests et prompts sûrs.
La gestion d’état React devient difficile quand toutes les valeurs changeantes sont traitées de la même façon. La valeur d’un champ, l’ouverture d’une modale, un panier, un thème utilisateur et une liste de produits chargée par API sont tous des états, mais ils n’ont ni le même propriétaire ni la même durée de vie.
Claude Code peut aider à remettre de l’ordre, à condition de lui donner une mission précise. “Refactorise le state management” est trop vague. Cela peut créer un store global trop large, dupliquer le cache serveur ou ajouter une librairie sans tests. Le bon flux consiste à demander un inventaire, séparer client state et server state, puis migrer une partie à la fois.
Ce guide explique quand React suffit, quand Zustand, Jotai ou TanStack Query aident vraiment, avec des exemples todo, panier, préférences et produits, des tests, des prompts sûrs, des liens officiels et un CTA de monétisation.
Classer l’état avant de choisir un outil
La première décision est la propriété de la donnée. Est-elle locale au composant, partagée dans le navigateur, persistée comme préférence, ou détenue par le serveur ?
flowchart TD
A["React state management"] --> B["Local UI state"]
A --> C["Shared client state"]
A --> D["Persisted preferences"]
A --> E["Server state"]
B --> B1["input, modal, tab"]
C --> C1["cart, wizard, editor"]
D --> D1["theme, density, locale"]
E --> E1["products, orders, profile from API"]
L’état local d’interface reste près du composant. useState suffit pour une valeur simple, useReducer pour plusieurs actions dans le même écran. L’état client partagé concerne des éléments comme un panier ou un éditeur. Les préférences persistées, comme le thème, doivent survivre au rechargement. Le server state vient d’une API et peut changer en dehors de votre application.
Quand React suffit
La documentation officielle Managing State recommande de structurer l’état, de le remonter si besoin, puis de combiner reducer et context quand l’écran grandit. Ce cadre est aussi utile pour guider Claude Code.
| Besoin | Premier choix | Envisager une librairie quand |
|---|---|---|
| Input, tab, modale | useState | Presque jamais |
| Écran avec plusieurs actions | useReducer | Le reducer sert à plusieurs routes |
| Prop drilling profond | Context | Les mises à jour fréquentes re-render trop |
| Panier ou brouillon | Zustand | Plusieurs zones éloignées modifient l’état |
| Thème et densité | Jotai | Beaucoup de petites préférences se combinent |
| Produits, commandes, profil API | TanStack Query | Cache, retry, refetch et invalidation sont nécessaires |
Si une valeur est utilisée dans moins de trois endroits, ne vient pas d’une API et ne doit pas survivre au rechargement, commencez avec React.
Cas 1 : Todo avec useReducer et Context
Une todo list a des actions claires : ajouter, basculer, supprimer. Sans synchronisation serveur, reducer et context sont un bon point de départ.
import {
createContext,
useContext,
useMemo,
useReducer,
type Dispatch,
type ReactNode,
} from "react";
export type Todo = {
id: string;
title: string;
done: boolean;
};
type TodoAction =
| { type: "added"; title: string }
| { type: "toggled"; id: string }
| { type: "deleted"; id: string };
export function todoReducer(todos: Todo[], action: TodoAction): Todo[] {
switch (action.type) {
case "added":
return [
...todos,
{ id: crypto.randomUUID(), title: action.title.trim(), done: false },
];
case "toggled":
return todos.map((todo) =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo,
);
case "deleted":
return todos.filter((todo) => todo.id !== action.id);
default:
return todos;
}
}
const TodoStateContext = createContext<Todo[] | null>(null);
const TodoDispatchContext = createContext<Dispatch<TodoAction> | null>(null);
export function TodoProvider({ children }: { children: ReactNode }) {
const [todos, dispatch] = useReducer(todoReducer, []);
const stateValue = useMemo(() => todos, [todos]);
return (
<TodoStateContext.Provider value={stateValue}>
<TodoDispatchContext.Provider value={dispatch}>
{children}
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
);
}
export function useTodos() {
const value = useContext(TodoStateContext);
if (value === null) throw new Error("useTodos must be used in TodoProvider");
return value;
}
export function useTodoDispatch() {
const value = useContext(TodoDispatchContext);
if (value === null) {
throw new Error("useTodoDispatch must be used in TodoProvider");
}
return value;
}
Cette forme aide Claude Code à travailler proprement. Vous pouvez demander une action edited, une validation contre les titres vides ou des tests du reducer sans toucher toute l’application.
Cas 2 : Panier avec Zustand
Un panier traverse la page produit, le header, le drawer et le checkout. Zustand est pratique pour un store léger sous forme de hook. Le middleware persist permet de garder seulement les champs sûrs après rechargement.
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
export type CartItem = {
productId: string;
name: string;
price: number;
quantity: number;
};
type CartState = {
items: CartItem[];
currency: "JPY" | "USD";
addItem: (item: Omit<CartItem, "quantity">) => void;
removeItem: (productId: string) => void;
setQuantity: (productId: string, quantity: number) => void;
clear: () => void;
total: () => number;
};
export const useCartStore = create<CartState>()(
persist(
(set, get) => ({
items: [],
currency: "USD",
addItem: (item) =>
set((state) => {
const current = state.items.find(
(entry) => entry.productId === item.productId,
);
if (current) {
return {
items: state.items.map((entry) =>
entry.productId === item.productId
? { ...entry, quantity: entry.quantity + 1 }
: entry,
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (productId) =>
set((state) => ({
items: state.items.filter((item) => item.productId !== productId),
})),
setQuantity: (productId, quantity) =>
set((state) => ({
items: state.items.map((item) =>
item.productId === productId
? { ...item, quantity: Math.max(1, quantity) }
: item,
),
})),
clear: () => set({ items: [] }),
total: () =>
get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}),
{
name: "cart-v1",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
items: state.items,
currency: state.currency,
}),
},
),
);
Le panier local n’est pas une source de vérité pour le paiement. Le serveur doit confirmer prix, stock, taxe, remise et paiement. Dites à Claude Code quels champs peuvent être persistés et quels champs doivent être validés par l’API.
Cas 3 : Préférences avec Jotai
Thème, densité, sidebar et preview mode sont de petites préférences. Jotai permet de les représenter comme atoms, puis de créer des valeurs dérivées.
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
export const themeAtom = atomWithStorage<"light" | "dark">(
"settings.theme",
"light",
);
export const densityAtom = atomWithStorage<"comfortable" | "compact">(
"settings.density",
"comfortable",
);
export const sidebarOpenAtom = atom(true);
export const settingsLabelAtom = atom((get) => {
const theme = get(themeAtom) === "dark" ? "dark theme" : "light theme";
const density =
get(densityAtom) === "compact" ? "compact layout" : "comfortable layout";
return `${theme}, ${density}`;
});
export const resetSettingsAtom = atom(null, (_get, set) => {
set(themeAtom, "light");
set(densityAtom, "comfortable");
set(sidebarOpenAtom, true);
});
Le risque est de tout découper en atoms. Jotai est excellent pour des préférences et des valeurs dérivées, pas pour remplacer un cache API ou cacher un workflow métier complexe.
Cas 4 : Produits et commandes avec TanStack Query
Les produits, commandes et profils sont du server state. Ils peuvent devenir obsolètes, échouer, être rechargés ou être modifiés ailleurs. TanStack Query v5 gère fetching, caching, synchronisation et mise à jour de ce type de données.
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import type { ReactNode } from "react";
type Product = {
id: string;
name: string;
price: number;
stock: number;
};
const queryClient = new QueryClient();
export function ProductsProvider({ children }: { children: ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
async function fetchProducts(category: string): Promise<Product[]> {
const response = await fetch(`/api/products?category=${category}`);
if (!response.ok) throw new Error("Failed to fetch products");
return response.json() as Promise<Product[]>;
}
async function addCartLine(productId: string) {
const response = await fetch("/api/cart/lines", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId }),
});
if (!response.ok) throw new Error("Failed to add cart line");
return response.json();
}
export function useProducts(category: string) {
return useQuery({
queryKey: ["products", category],
queryFn: () => fetchProducts(category),
staleTime: 60_000,
});
}
export function useAddCartLine() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addCartLine,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cart"] });
},
});
}
La frontière est simple : Zustand peut garder l’intention temporaire du panier, mais TanStack Query doit lire les produits, commandes, panier confirmé et checkout depuis le serveur.
Prompts sûrs et tests
La page How Claude Code works décrit le cycle contexte, action, vérification. Pour un refactor d’état, demandez d’abord un audit sans modification.
Inspecte la gestion d'état de cette app React. Ne modifie pas encore les fichiers.
Objectifs:
- séparer client state et server state
- identifier ce qui peut rester en useState/useReducer/Context
- justifier Zustand, Jotai ou TanStack Query si nécessaire
Retour attendu:
- inventaire des états
- état dupliqué ou dérivé
- données API stockées comme client state
- ordre de migration sûr avec tests
Testez ensuite la logique pure avant l’interface complète.
import { describe, expect, it, vi } from "vitest";
import { todoReducer, type Todo } from "./TodoProvider";
describe("todoReducer", () => {
it("adds a todo with a generated id", () => {
vi.spyOn(crypto, "randomUUID").mockReturnValue("todo-1");
const result = todoReducer([], { type: "added", title: "Write tests" });
expect(result).toEqual([
{ id: "todo-1", title: "Write tests", done: false },
]);
});
it("toggles a todo", () => {
const initial: Todo[] = [{ id: "todo-1", title: "Ship", done: false }];
const result = todoReducer(initial, { type: "toggled", id: "todo-1" });
expect(result[0].done).toBe(true);
});
});
Pour TanStack Query, suivez le testing guide : créez un QueryClient neuf par test pour éviter les fuites de cache.
Pièges, liens et CTA
Les pièges fréquents sont clairs : tout mettre dans un store global, stocker du server state dans Zustand, dupliquer des valeurs dérivées, persister des informations sensibles, ou accepter le diff de Claude Code sans test, build et vérification manuelle après reload et erreur API.
Références officielles : React Managing State, Zustand persist, Jotai, TanStack Query overview, Claude Code best practices.
Pour améliorer les instructions, lisez le guide de prompts et les conseils de productivité Claude Code. Pour des modèles réutilisables, voyez les produits. Pour appliquer ce workflow à un vrai dépôt d’équipe, utilisez la page training et consultation.
Après essai, le meilleur gain est venu du déplacement du server state vers TanStack Query en premier. Zustand est resté petit, Jotai s’est limité aux préférences, et les changements produits par Claude Code étaient plus faciles à relire grâce à l’inventaire initial.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Échelle de sécurité des permissions Claude Code
Passer du read-only aux éditions limitées, preuves et checks de déploiement sans perdre le contrôle.
Claude Code Small PR Proof Pack : rendre les petits changements reviewables
Un pack de preuve pour PR Claude Code : diff, vérifications, URL publique, CTA et rollback.
Gate de review avant commit avec Claude Code
Review avant commit avec Claude Code : diff, build, URL publique, liens Gumroad, CTA consultation, tests manquants et fichiers hors scope.