Il existe un réflexe bien installé dans l'écosystème React : dès qu'une application dépasse trois écrans, on ajoute une bibliothèque de gestion d'état. Le tutoriel suivi la veille en utilisait une, les offres d'emploi la mentionnent, la décision se prend en trente secondes et personne ne la remet en question.
Six mois plus tard, le store contient des données qui viennent d'une API, un indicateur de chargement dupliqué à trois endroits, un booléen pour l'ouverture d'une modale, et une fonction de rafraîchissement manuel écrite parce que le cache ne se met plus à jour. Chaque nouvelle fonctionnalité demande de toucher quatre fichiers avant d'afficher une ligne de texte.
La documentation officielle de Redux dit elle-même depuis des années qu'il ne faut pas l'utiliser sans besoin identifié. Reste à savoir ce qu'est un besoin identifié. C'est ce que cet article essaie de rendre décidable, avec une méthode de tri qui tient en quatre catégories.
Le store qui gonfle sans qu'on l'ait décidé
Prenons une application de gestion tout à fait ordinaire : une liste de clients avec recherche et filtres, une fiche détaillée, un formulaire d'édition, un thème sombre et un utilisateur connecté. Sur les projets de reconversion ou de première mission qu'on me demande de relire, le store global contient à peu près ceci :
{
clients: [], // vient de l'API
clientsLoading: false,
clientsError: null,
selectedClientId: null,
searchTerm: "",
currentPage: 1,
isModalOpen: false,
theme: "light",
user: { id: 12, name: "..." }
}
Neuf entrées dans le même sac, alors qu'elles n'ont ni la même durée de vie, ni la même source de vérité, ni les mêmes besoins de synchronisation. Trois d'entre elles décrivent l'état d'une requête réseau, deux devraient se lire dans l'URL, une n'intéresse qu'un seul composant, et deux seulement méritent d'être globales.
Les symptômes arrivent ensuite dans un ordre assez prévisible. Un utilisateur partage un lien vers une liste filtrée, le destinataire voit la liste non filtrée. Deux onglets ouverts affichent des données divergentes. Un bouton de suppression fonctionne mais la liste ne se met à jour qu'après un rechargement complet, ce qu'on corrige avec un appel de rafraîchissement placé à la main, puis un deuxième ailleurs.
Quatre familles d'état, quatre réponses
Avant de choisir un outil, il faut trier. La question à poser sur chaque valeur : où vit la vérité de cette donnée ?
1. L'état serveur
Une liste de clients, un profil, un panier stocké côté back : la vérité est dans une base de données, pas dans le navigateur. Ce que tu détiens côté client est une copie potentiellement périmée. La question à traiter n'est donc pas le stockage, c'est le cache, la revalidation, la déduplication des requêtes et l'état de chargement. Un store généraliste ne sait rien faire de tout ça sans code écrit à la main.
2. L'état d'URL
Un terme de recherche, un numéro de page, un onglet actif, un ensemble de filtres. Ces valeurs doivent survivre à un rechargement, tenir dans un lien partagé et fonctionner avec le bouton retour du navigateur. L'URL fait tout ça gratuitement, un store non.
3. L'état local
L'ouverture d'un menu déroulant, le contenu d'un champ pendant la saisie, un survol. Personne d'autre que le composant concerné n'a besoin de le lire. useState couvre cette famille entièrement, et remonter cet état plus haut que nécessaire est la première source de re-rendus inutiles.
4. L'état global client
Le thème, la langue d'affichage, l'ouverture d'un panneau latéral, parfois un panier avant validation. Cette famille est petite dans presque toutes les applications, et c'est la seule où la question du store global se pose sérieusement.
Le code de chaque famille
État serveur avec un Server Component
Dans le App Router de Next.js, un composant serveur récupère les données pendant le rendu. Aucun état de chargement à gérer côté client, aucun store à alimenter.
// app/clients/page.tsx
export default async function ClientsPage() {
const clients = await db.client.findMany();
return <ClientList clients={clients} />;
}
Trois entrées du store initial disparaissent d'un coup : les données, l'indicateur de chargement et l'erreur. Le fichier qui les gérait n'existe plus.
État serveur côté client avec TanStack Query
Quand les données doivent se rafraîchir sans navigation, une bibliothèque spécialisée dans le cache fait le travail.
const { data, isPending, error } = useQuery({
queryKey: ["clients", page],
queryFn: () => fetch(`/api/clients?page=${page}`).then(r => r.json()),
staleTime: 30_000,
});
Déduplication des requêtes identiques, revalidation au retour de l'onglet, invalidation ciblée après une mutation : tout cela est fourni. Dans un store généraliste, chacun de ces comportements devient du code que tu écris et que tu maintiens.
État d'URL avec les paramètres de recherche
"use client";
import { useRouter, useSearchParams } from "next/navigation";
export function SearchBox() {
const router = useRouter();
const params = useSearchParams();
const term = params.get("q") ?? "";
function update(value: string) {
const next = new URLSearchParams(params);
value ? next.set("q", value) : next.delete("q");
router.replace(`?${next}`);
}
return <input value={term} onChange={e => update(e.target.value)} />;
}
Le lien devient partageable, le bouton retour retrouve le filtre précédent, et un rechargement de page restitue l'écran à l'identique. Aucun store ne donne ces trois propriétés sans travail supplémentaire.
État global client avec Zustand
Pour la quatrième famille, un store minimal suffit dans la majorité des applications.
import { create } from "zustand";
export const useUiStore = create((set) => ({
theme: "light",
sidebarOpen: false,
toggleTheme: () => set(s => ({ theme: s.theme === "light" ? "dark" : "light" })),
toggleSidebar: () => set(s => ({ sidebarOpen: !s.sidebarOpen })),
}));
// dans un composant
const theme = useUiStore(s => s.theme);
La sélection par fonction évite qu'un composant qui lit le thème se re-rende quand la barre latérale s'ouvre. Ce point est le principal avantage pratique d'un store externe sur le Context de React.
Les formulaires depuis React 19
React 19, sorti en décembre 2024 et arrivé au patch 19.2.8 en juillet 2026, a absorbé une partie de ce qu'on écrivait à la main dans les stores : état d'envoi, erreur, mise à jour optimiste.
const [state, formAction, isPending] = useActionState(saveClient, null);
return (
<form action={formAction}>
<input name="name" />
<button disabled={isPending}>Enregistrer</button>
{state?.error && <p>{state.error}</p>}
</form>
);
Le React Compiler, stable depuis octobre 2025, retire de son côté une bonne partie des useMemo et useCallback qu'on plaçait par précaution. Ces deux évolutions expliquent pourquoi un conseil de 2021 sur la gestion d'état ne s'applique plus tel quel.
Les cas où Redux reste le bon choix
Rien de ce qui précède ne dit que Redux serait dépassé. Redux Toolkit est maintenu, documenté, et il répond très bien à des besoins que les alternatives légères ne couvrent pas :
- Un état client volumineux, avec des dizaines de transitions et des dépendances croisées entre domaines. Un éditeur de documents, un outil de dessin, un tableau de bord temps réel avec plusieurs flux.
- Un besoin de traçabilité. Les DevTools Redux rejouent l'historique des actions, ce qui rend un bug reproductible à partir d'un rapport utilisateur. Sur un logiciel métier, cette capacité vaut son coût.
- Une équipe nombreuse sur la même base de code, où la contrainte structurelle imposée par les slices et les reducers vaut mieux qu'un store libre où chacun invente sa convention.
- Une application existante déjà construite dessus. Migrer un store Redux qui fonctionne vers autre chose parce que le vent a tourné est rarement rentable, et c'est le genre d'arbitrage évoqué dans l'article sur la rentabilité de la formation aux frameworks.
Mon avis, à prendre pour ce qu'il vaut : installer Redux par défaut dans un projet Next.js App Router démarré en 2026 est un choix daté. Sur une application de gestion classique, le tri par familles laisse rarement plus de deux ou trois valeurs dans la quatrième catégorie, et deux valeurs ne justifient pas une architecture à middleware.
Les pièges classiques
Stocker de l'état dérivé
Garder clients et filteredClients côte à côte crée deux sources de vérité qui finiront par diverger. Une valeur calculable à partir d'une autre se calcule au rendu. La règle vaut aussi pour un total, un compteur ou un booléen de validité.
Synchroniser deux états avec useEffect
Un effet qui lit une valeur pour en écrire une autre déclenche un rendu supplémentaire et introduit un décalage d'une frame. Dans presque tous les cas, la valeur doit être calculée pendant le rendu ou remontée au parent commun.
Un Context unique pour toute l'application
Le Context de React ne propose pas de sélecteur : tout consommateur se re-rend quand la valeur change, même s'il ne lit qu'un champ. Un contexte qui contient à la fois l'utilisateur, le thème et l'état d'un panneau fait travailler l'arbre entier au moindre clic. Deux issues : séparer en plusieurs contextes indépendants, ou passer à un store avec sélecteurs.
Traiter le prop drilling comme une fatalité
Passer une prop sur deux niveaux ne justifie pas un store global. La composition règle souvent le cas : au lieu de faire descendre une donnée à travers des composants intermédiaires, passe l'élément déjà construit en children. Les composants du milieu n'ont plus rien à connaître.
Mettre l'état serveur dans le store global
C'est le piège le plus coûteux, parce qu'il ne se voit pas au début. Tout va bien jusqu'à la première mutation depuis un autre écran, et à partir de là tu écris ta propre invalidation de cache. La gestion de cache est un problème réputé difficile, et le résoudre à moitié dans un coin de reducer coûte plus cher que d'adopter un outil qui le traite. La séparation entre les données persistées et la logique d'affichage rejoint ce qui est décrit dans l'article sur le repository pattern, côté back cette fois.
Se former sur ces choix d'architecture
Ces arbitrages s'apprennent en construisant, pas en lisant des comparatifs. La formation React : développer des interfaces modernes traite les hooks, la composition et la gestion d'état locale sur des cas réels. La formation Next.js : construire des applications web modernes prend la suite avec le App Router, les Server Components et le rendu côté serveur, là où une grande partie de la question du state management se dissout. Le reste du parcours figure dans la catégorie Développement Frontend.
Questions fréquentes
Redux est-il mort ?
Faut-il apprendre Redux quand on cherche un premier poste ?
Zustand ou Context, comment choisir ?
Les Server Components suppriment-ils le besoin de state management ?
Comment migrer un store existant sans tout casser ?
Un projet de portfolio a-t-il besoin d'un store global ?
Un exercice utile sur ton projet actuel : ouvre ton store et note en face de chaque entrée à quelle famille elle appartient. Les lignes que tu n'arrives pas à classer sont en général celles qui posent problème.
Pour la qualité du code autour de ces choix, l'article sur les 20 principes de code donne le cadre général, et celui sur les erreurs de typage qui révèlent un mauvais design traite d'un symptôme voisin côté TypeScript.