La gestion de l'état est au cœur de chaque application React non triviale. Faites les choses correctement et votre base de code évolue gracieusement ; Si vous vous trompez, vous vous retrouverez avec des cauchemars de forage d'accessoires, une interface utilisateur obsolète et des composants qui se retrouveront dans l'oubli. La bonne nouvelle est que l’écosystème React a considérablement mûri. Vous disposez désormais d'un riche menu d'outils spécialement conçus - Redux Toolkit, Zustand, Jotai, TanStack Query et Context API intégré - chacun optimisé pour une classe spécifique de problèmes.
Cet article examine chaque solution en profondeur, explique les compromis avec des exemples concrets et vous donne un cadre décisionnel que vous pouvez appliquer immédiatement à vos propres projets.
Comprendre les différents types d'État
Avant de rechercher une bibliothèque, il convient d’être précis sur le type d’état que vous gérez réellement. La fusion de différentes catégories d'états est à l'origine de la plupart des applications React sur-conçues.
- État de l'interface utilisateur locale — si une liste déroulante est ouverte, quel onglet est actif, la valeur actuelle d'une entrée contrôlée. Cet état appartient à un seul composant ou à un petit sous-arbre.
- État du client partagé — des données que plusieurs composants potentiellement distants doivent lire ou écrire : l'utilisateur actuellement authentifié, les préférences de thème, un panier d'achat.
- État du serveur — les données provenant d'un serveur, sont asynchrones par nature et nécessitent une invalidation du cache, une nouvelle récupération en arrière-plan et une gestion du cycle de vie des chargements/erreurs.
- État de l'URL - des filtres, une pagination et des termes de recherche qui doivent survivre à une actualisation de page et pouvoir être partagés via un lien.
- État du formulaire — entrées contrôlées, erreurs de validation et statut de soumission, souvent mieux gérées par une bibliothèque dédiée telle que React Hook Form.
La chose la plus efficace que vous puissiez faire est de résister à la tentation de regrouper toutes les catégories d’État dans un seul magasin mondial. Chaque catégorie a des exigences de cohérence différentes, des durées de vie différentes et des fréquences de mise à jour différentes.
React Context : l'option intégrée
Le Context API est livré avec React et ne nécessite aucune dépendance supplémentaire. Cela fonctionne bien pour les états qui changent rarement et sont consommés par de nombreux composants – des exemples classiques sont le thème, les paramètres régionaux et l'objet utilisateur authentifié.
// AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface AuthState {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (credentials: Credentials) => {
const user = await authService.login(credentials);
setUser(user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used inside AuthProvider');
return ctx;
}
Le piège des performances contextuelles
Le contexte a une caractéristique de performance bien connue qui fait trébucher de nombreux développeurs : chaque consommateur effectue un nouveau rendu chaque fois que la référence de la valeur du contexte change. Si vous stockez un objet volumineux et fréquemment muté dans un seul contexte, vous déclencherez des rendus inutiles dans l'ensemble de votre arborescence.
Les stratégies d'atténuation sont les suivantes :
- Divisez les contextes par fréquence de mise à jour. Conservez l'objet utilisateur dans un contexte et les préférences de l'interface utilisateur dans un autre.
- Mémorisez l'objet de valeur avec
useMemodonc la référence ne change que lorsque les données changent réellement. - Utiliser
React.memosur les composants consommateurs pour éviter les nouveaux rendus lorsque les accessoires qui les intéressent n'ont pas changé.
La conclusion honnête est que Context est excellent pour l’état global à basse fréquence, mais ce n’est pas une solution de gestion d’état à usage général. Une fois que vous vous retrouvez à écrire une mémorisation élaborée pour contourner le comportement de re-rendu de Context, il est temps de recourir à une bibliothèque dédiée.
Boîte à outils Redux : le choix d'une entreprise mature
Redux a une réputation de passe-partout – une réputation qu'il a acquise à l'époque pré-Toolkit. Redux Toolkit (RTK) élimine la cérémonie tout en préservant ce qui rend Redux puissant : une arborescence d'état unique, prévisible et inspectable avec un flux de données unidirectionnel strict.
Navires RTK createSlice, qui génère des créateurs et des réducteurs d'action à partir d'une seule définition, et utilise Immer sous le capot afin que vous puissiez écrire des mutations qui semblent impératives mais qui sont en réalité appliquées de manière immuable.
// features/cart/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
quantity: number;
price: number;
}
interface CartState {
items: CartItem[];
coupon: string | null;
}
const initialState: CartState = { items: [], coupon: null };
export const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem(state, action: PayloadAction<CartItem>) {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.quantity += action.payload.quantity;
} else {
state.items.push(action.payload);
}
},
removeItem(state, action: PayloadAction<string>) {
state.items = state.items.filter(i => i.id !== action.payload);
},
applyCoupon(state, action: PayloadAction<string>) {
state.coupon = action.payload;
},
},
});
export const { addItem, removeItem, applyCoupon } = cartSlice.actions;
export default cartSlice.reducer;
Requête RTK : état du serveur dans Redux
RTK fournit un compagnon appelé RTK Query qui gère l'état du serveur directement dans le magasin Redux. Il génère automatiquement des hooks React à partir des définitions de points de terminaison et gère la mise en cache, l'invalidation et les mises à jour optimistes.
// services/productsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Product'],
endpoints: (builder) => ({
getProducts: builder.query<Product[], void>({
query: () => '/products',
providesTags: ['Product'],
}),
updateProduct: builder.mutation<Product, Partial<Product>>({
query: ({ id, ...patch }) => ({
url: `/products/${id}`,
method: 'PATCH',
body: patch,
}),
invalidatesTags: ['Product'],
}),
}),
});
export const { useGetProductsQuery, useUpdateProductMutation } = productsApi;
Redux Toolkit est le bon choix lorsque vous avez besoin d'un débogage temporel via Redux DevTools, lorsque plusieurs équipes doivent contribuer à un modèle d'état partagé avec des conventions appliquées, ou lorsque vous disposez d'une logique métier multi-tranches complexe qui bénéficie d'un middleware tel que redux-saga ou redux-observable.
Zustand : un État mondial léger sans cérémonie
Zustand adopte une approche radicalement différente. Il n'y a ni fournisseur, ni réducteur, ni type d'action. Vous définissez un magasin comme un simple objet JavaScript avec un état et des méthodes, puis vous le consommez avec un seul hook. L'ensemble du API tient sur un seul écran.
// stores/useCartStore.ts
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
totalPrice: () => number;
clear: () => void;
}
export const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + item.quantity }
: i
),
};
}
return { items: [...state.items, item] };
}),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
totalPrice: () =>
get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
clear: () => set({ items: [] }),
}),
{ name: 'cart-storage' }
)
)
);
Abonnements granulaires et performances
Zustand résout le problème de rendu de Context grâce à des abonnements basés sur des sélecteurs. Un composant n'est restitué que lorsque la tranche d'état qu'il a sélectionnée a changé.
// Only re-renders when items.length changes, not on price changes
const itemCount = useCartStore((state) => state.items.length);
// Only re-renders when the total changes
const total = useCartStore((state) => state.totalPrice());
L'écosystème middleware de Zustand couvre la persistance jusqu'à localStorage, intégration DevTools, mutations de style Immer et synchronisation d'URL. Il s'agit du choix idéal pour les équipes qui souhaitent des fonctionnalités de niveau Redux sans frais d'installation de niveau Redux.
Jotai : État atomique inspiré par le recul
Les modèles Jotai se présentent sous la forme d’un graphique d’atomes – de minuscules unités d’état composables qui peuvent être dérivées les unes des autres. Ce modèle est particulièrement puissant pour les états dynamiques : pensez à une liste de nœuds d'éditeur où chaque nœud a son propre état de sélection/focus indépendant, ou à une forme complexe où la visibilité du champ dépend d'autres valeurs de champ.
// atoms/filterAtoms.ts
import { atom, selector } from 'jotai';
export const searchTermAtom = atom('');
export const categoryAtom = atom<string | null>(null);
export const productsAtom = atom<Product[]>([]);
// Derived atom — recomputes only when its dependencies change
export const filteredProductsAtom = atom((get) => {
const products = get(productsAtom);
const term = get(searchTermAtom).toLowerCase();
const category = get(categoryAtom);
return products.filter((p) => {
const matchesTerm = p.name.toLowerCase().includes(term);
const matchesCategory = category === null || p.category === category;
return matchesTerm && matchesCategory;
});
});
// ProductList.tsx
import { useAtom, useAtomValue } from 'jotai';
function ProductList() {
const [searchTerm, setSearchTerm] = useAtom(searchTermAtom);
const filtered = useAtomValue(filteredProductsAtom);
return (
<>
<input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
{filtered.map((p) => <ProductCard key={p.id} product={p} />)}
</>
);
}
Le modèle de réactivité à granularité fine de Jotai signifie que lorsque le terme de recherche change, seuls les composants qui s'abonnent réellement à filteredProductsAtom re-rendu. Composants souscrits uniquement à categoryAtom sont intacts. Cela rend Jotai excellent pour les applications avec des graphes d'état denses et interdépendants.
Requête TanStack : la bonne solution pour l'état du serveur
TanStack Query (anciennement React Query) n'est pas une bibliothèque générale de gestion d'état - c'est une bibliothèque d'état de serveur, et elle gère cette responsabilité mieux que toute autre chose dans l'écosystème. Récupérer, mettre en cache, synchroniser et mettre à jour des données distantes dans React sans TanStack Query signifie généralement gérer manuellement les indicateurs de chargement, les objets d'erreur et l'invalidation du cache dans un état local ou dans un magasin global. TanStack Query automatise tout cela.
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function useProducts(filters: ProductFilters) {
return useQuery({
queryKey: ['products', filters],
queryFn: () => api.getProducts(filters),
staleTime: 5 * 60 * 1000, // treat data as fresh for 5 minutes
gcTime: 10 * 60 * 1000, // keep unused data in cache for 10 minutes
});
}
export function useUpdateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (update: ProductUpdate) => api.updateProduct(update),
// Optimistic update
onMutate: async (update) => {
await queryClient.cancelQueries({ queryKey: ['products'] });
const previous = queryClient.getQueryData(['products']);
queryClient.setQueryData(['products'], (old: Product[]) =>
old.map((p) => (p.id === update.id ? { ...p, ...update } : p))
);
return { previous };
},
onError: (_err, _update, context) => {
queryClient.setQueryData(['products'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
Le tableau de clés de requête est la primitive de mise en cache de TanStack Query. Toute requête partageant la même clé partage la même entrée de cache. Lorsque vous invalidez une clé, chaque composant abonné à cette clé est automatiquement récupéré en arrière-plan et mis à jour lorsque de nouvelles données arrivent.
Prélecture et hydratation dans Next.js
Dans une application Next.js, vous pouvez pré-extraire les requêtes sur le serveur et les déshydrater dans la charge utile HTML, éliminant ainsi complètement le chargement des spinners pour le rendu initial de la page.
// app/products/page.tsx (Next.js App Router)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
export default async function ProductsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['products', {}],
queryFn: () => api.getProducts({}),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProductList />
</HydrationBoundary>
);
}
Combiner les bibliothèques : une architecture pratique
Les applications réelles bénéficient presque toujours de la combinaison de plusieurs outils, chacun responsable de sa propre catégorie d'état. Une application React de taille moyenne à grande bien structurée pourrait ressembler à ceci :
- Requête TanStack possède tous les états du serveur. Il récupère, met en cache et synchronise les données distantes. Rien d'autre ne touche les réponses API.
- Zustand possède l'état global du client : le panier, la session utilisateur authentifiée, les préférences de l'interface utilisateur et toute coordination inter-fonctionnalités trop complexe pour le contexte.
- Contexte de réaction possède une configuration à faible taux de désabonnement : le thème actuel, les paramètres régionaux, les indicateurs de fonctionnalité.
- useState / useRéducteur propre état du composant local – modal ouvert/fermé, valeurs des champs de formulaire (ou React Hook Form pour les formulaires complexes), sélection par onglets.
- Paramètres de recherche d'URL propre état de navigation : filtres actifs, ordre de tri, numéro de page actuel.
Cette séparation des préoccupations n’est pas seulement esthétique. Cela signifie que votre cache d'état de serveur n'est jamais pollué par l'état de l'interface utilisateur, que votre magasin client global ne gonfle jamais avec des données de réponse que TanStack Query mettrait en cache plus efficacement, et que votre contexte ne déclenche jamais de nouveaux rendus à l'échelle de l'application parce que quelqu'un y a stocké une valeur en évolution rapide.
Modèles de performances à connaître
Sélecteurs mémorisés avec resélection
Lorsque vous dérivez des valeurs calculées à partir d'un magasin Redux, utilisez Reselect pour éviter de recalculer à chaque rendu. Réexportations RTK createSelector à partir de Résélectionner.
import { createSelector } from '@reduxjs/toolkit';
const selectItems = (state: RootState) => state.cart.items;
export const selectCartSummary = createSelector(selectItems, (items) => ({
count: items.reduce((n, i) => n + i.quantity, 0),
total: items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}));
Fractionner les rendus avec useTransition
Réagissez les 18 useTransition vous permet de marquer une mise à jour d'état comme non urgente afin que le navigateur puisse l'interrompre pour gérer un travail plus prioritaire comme la saisie de l'utilisateur. Ceci est particulièrement utile lorsqu'un changement d'état déclenche un nouveau rendu coûteux.
const [isPending, startTransition] = useTransition();
const handleFilterChange = (value: string) => {
startTransition(() => {
setFilterValue(value); // expensive downstream re-render is deferrable
});
};
Éviter les problèmes d'identité d'objet
Une source courante de rendus fantômes consiste à créer de nouveaux littéraux d'objet ou de tableau dans le rendu. useMemo et useCallback sont vos outils ici, mais appliquez-les uniquement lorsque vous avez des preuves d'un problème de performances - la mémorisation prématurée ajoute une surcharge cognitive sans bénéfice garanti.
Cadre décisionnel
Utilisez l'arbre de décision suivant lors du choix d'une approche de gestion d'état pour une nouvelle fonctionnalité :
- L'état est-il purement local à un composant ou à un petit sous-arbre ? Utiliser
useStateouuseReducer. - L'état représente-t-il les données extraites d'un serveur ? Utiliser Requête TanStack.
- S'agit-il d'un état client global dont de nombreux composants de l'arborescence ont besoin ? Atteindre Zustand sauf si vous êtes déjà dans une base de code Redux.
- L'état change-t-il rarement et sert-il à un objectif de configuration (thème, paramètres régionaux) ? Utiliser Contexte de réaction.
- L’État est-il fortement interconnecté avec de nombreuses valeurs dérivées ? Considérer Jotai.
- Avez-vous besoin d'un débogage de voyages dans le temps, de conventions architecturales strictes ou de sagas asynchrones complexes ? Utiliser Boîte à outils Redux.
Anti-modèles courants à éviter
- Stockage des réponses du serveur dans Redux ou Zustand. Laissez TanStack Query posséder ces données. Le dupliquer dans un store global crée des bugs de synchronisation.
- Tout regrouper dans un seul grand magasin. Un état non lié appartenant à la même tranche force un couplage inutile et rend les tests plus difficiles.
- Utilisation de Context pour les mises à jour à haute fréquence. Chaque changement de valeur de contexte restitue tous les consommateurs. Pour les valeurs qui changent à chaque frappe ou image d’animation, Context n’est pas le bon outil.
- Dérive de l’état à l’intérieur des composants. Les valeurs calculées qui dépendent de l'état doivent être des sélecteurs mémorisés ou des atomes dérivés, et non des calculs en ligne exécutés à chaque rendu.
- Ignorer les états de chargement et d’erreur. L'état du serveur est intrinsèquement asynchrone. Surfaces de requête TanStack
isLoading,isError, eterrorpour chaque requête ; utilisez-les.
Conclusion
Le paysage de la gestion étatique dans React n’a jamais été aussi sain. Vous n’avez plus besoin de choisir entre un framework lourd et tout construire à partir de zéro. Redux Toolkit offre une prévisibilité de niveau entreprise sans le passe-partout de son prédécesseur. Zustand apporte la même puissance avec une fraction de la configuration. Jotai résout les problèmes difficiles dans les graphes d’état dynamiques et à granularité fine. Et TanStack Query a définitivement résolu l'état du serveur - si vous gérez toujours les réponses API dans useEffect et useState, vous vous devez de migrer.
L’information la plus importante est catégorique : identifiez d’abord le type d’état auquel vous faites face, puis sélectionnez l’outil optimisé pour cette catégorie. Résistez à l’instinct de tout regrouper dans un seul système. Une approche en couches - TanStack Query pour l'état du serveur, Zustand pour l'état global du client, Contexte pour la configuration et état local pour tout le reste - produit des bases de code plus faciles à raisonner, plus faciles à tester et beaucoup plus maintenables à mesure que les équipes et les exigences grandissent.