Aller au contenu principal
MEWA STUDIO

State management : Redux vs Zustand vs Context API

Publié le 3 juillet 2026|11 min de lecture
développementJavaScriptperformance

Choisir un gestionnaire d'état, ce n'est pas trouver le meilleur outil mais faire correspondre le bon outil au bon type d'état. Context API, Redux Toolkit et Zustand comparés sur ce qui compte vraiment : le piège de re-render, le code à écrire, le terrain idéal de chacun, puis une méthode pour décider.

Traits bleus lumineux sur fond noir, illustration abstraite

Un clic sur une case à cocher et toute la page se recalcule. Un panier qui perd son contenu au changement de page. Une même donnée dupliquée dans trois composants qui finissent par ne plus être d'accord. Sur beaucoup d'applications, ces symptômes sont mis sur le compte de React lui-même. Le vrai sujet est ailleurs : l'état a été rangé au mauvais endroit, avec le mauvais outil.

Choisir un gestionnaire d'état n'est pas choisir le meilleur outil dans l'absolu. C'est faire correspondre un type d'état à l'outil qui le gère sans effort inutile ni piège de performance. La plupart des applications se trompent dans un sens ou dans l'autre : elles sortent l'artillerie d'un Redux là où trois lignes suffisaient ou détournent le Context API en gestionnaire global jusqu'à ce que chaque interaction repeigne l'écran entier.

Aujourd'hui, on tranche entre les trois options les plus répandues de l'écosystème React : le Context API, Redux dans sa forme moderne Redux Toolkit et Zustand. On commence par la seule question qui compte, on dissèque le piège de re-render qui plombe la plupart des implémentations, puis on donne une méthode pour décider.

La seule question qui compte : de quel état parle-t-on ?

Avant de comparer les outils, il faut trier l'état. Le mot recouvre des réalités si différentes qu'aucun outil unique ne les gère toutes correctement. Trois familles se distinguent.

  • L'état local : une case cochée, un champ de formulaire, un menu ouvert ou fermé. Il ne concerne qu'un composant et sa descendance immédiate. useState et useReducer suffisent et c'est presque toujours le bon choix.
  • L'état client global : le thème, la langue, l'utilisateur connecté, le contenu d'un panier, l'état d'une interface complexe partagée entre des composants éloignés. C'est le terrain de jeu réel des trois outils comparés ici.
  • L'état serveur : les données qui vivent dans une base et que l'application récupère par requête. Produits, articles, profil. Ce n'est pas un état à gérer mais un cache à synchroniser et c'est la source de la plus grosse erreur du domaine.

La confusion la plus coûteuse consiste à ranger l'état serveur dans un Redux ou un Context. On se retrouve alors à réimplémenter à la main ce qu'une bibliothèque de data-fetching fait déjà : la mise en cache, la déduplication des requêtes, la revalidation, la gestion du chargement et de l'erreur. Pour cet état, des outils comme TanStack Query ou SWR sont faits pour ça et déchargent le gestionnaire d'état client de données qui n'ont rien à y faire. La règle : un gestionnaire d'état client ne stocke que de l'état client. Le reste est du cache serveur.

Depuis l'App Router et les React Server Components, une part de ce qui passait par un état global n'a même plus besoin d'exister côté client. Une donnée lue au rendu serveur, un filtre porté par l'URL, un état d'authentification résolu côté serveur : autant de cas qui allègent le besoin en état client avant même de choisir un outil. La première question reste donc : cet état a-t-il vraiment besoin de vivre dans le navigateur ?

Context API : de l'injection de dépendances, pas un gestionnaire d'état

Le Context API est souvent présenté comme la solution native de React au partage d'état. C'est une lecture trompeuse. Le Context résout un problème précis : faire descendre une valeur dans l'arbre des composants sans la passer manuellement de parent en enfant, ce qu'on appelle le prop drilling. C'est un mécanisme de distribution, pas de gestion. Il ne sait ni optimiser les mises à jour ni s'abonner à une portion de la donnée.

D'où son piège le plus connu. Quand la valeur d'un Provider change, tous les composants qui consomment ce Context se re-rendent, qu'ils utilisent ou non la partie qui a changé. Un Context qui contiendrait à la fois le thème, l'utilisateur et le contenu du panier ferait repeindre l'écran du panier à chaque changement de thème et inversement. Il n'existe aucun moyen natif de s'abonner à user sans être notifié des changements de cart.

On atténue le problème sans le supprimer : découper en plusieurs Contexts indépendants pour que chaque mise à jour ne touche que ses vrais consommateurs, mémoïser la valeur passée au Provider avec useMemo pour éviter de créer un nouvel objet à chaque rendu du parent, séparer la valeur qui change souvent de celle qui reste stable. Ces techniques repoussent la limite, elles ne changent pas la nature de l'outil.

Le Context est excellent pour ce à quoi il est destiné : une valeur globale qui change rarement et que beaucoup de composants lisent. Thème, langue, utilisateur authentifié, identité d'un client en marque blanche. Pour un état qui change plusieurs fois par seconde au fil des interactions, il devient un générateur de re-renders. C'est là que les gestionnaires à sélecteurs prennent le relais.

Redux Toolkit : le conteneur prévisible quand la complexité l'exige

Redux repose sur un principe strict : un état global unique, modifié uniquement par des actions passées à des reducers, fonctions pures qui décrivent comment l'état se transforme. Cette discipline a un coût en code répétitif à écrire mais offre une garantie rare : à tout instant, on sait d'où vient chaque changement d'état et pourquoi.

Reproché pendant des années pour sa verbosité, Redux se pratique aujourd'hui via Redux Toolkit, sa forme officielle et recommandée. createSlice génère actions et reducers d'un bloc, Immer autorise une écriture qui a l'air mutable tout en restant immuable sous le capot et configureStore câble les DevTools et les middlewares sans configuration manuelle. La documentation de Redux Toolkit (opens in a new tab) en fait le point d'entrée par défaut, le Redux écrit à la main des tutoriels d'il y a cinq ans n'est plus la norme.

Côté performance, useSelector s'abonne à une portion précise de l'état et ne re-rend le composant que si cette portion change. Redux échappe donc nativement au piège du Context. La contrepartie est qu'un sélecteur qui renvoie un nouvel objet à chaque appel casse la comparaison par référence et re-rend quand même. On dérive alors la donnée avec des sélecteurs mémoïsés, via Reselect intégré à la boîte à outils.

Cette lourdeur de Redux devient rentable dans des conditions précises : une application vaste, plusieurs développeurs qui ont besoin de conventions partagées, un état riche aux dépendances croisées, un besoin d'audit ou de débogage fin que les DevTools de Redux offrent avec le voyage dans le temps action par action, une logique asynchrone complexe orchestrée par des middlewares. Sur une petite application, ce même code répétitif est un coût sans contrepartie.

Zustand : l'état global sans la lourdeur

Zustand occupe le terrain entre les deux. Il offre un magasin global comme Redux mais avec une surface d'API minimale et sans Provider à câbler autour de l'application. On déclare un store avec un hook, on y range état et fonctions de mise à jour et chaque composant s'abonne à la portion qui l'intéresse via un sélecteur.

C'est là son atout décisif face au Context : l'abonnement est granulaire par défaut. Un composant qui lit cart n'est pas re-rendu quand theme change, sans découpage manuel ni Provider multiple. Le store vit en dehors de l'arbre React, ce qui autorise aussi des lectures et écritures hors composant et des mises à jour transitoires qui ne déclenchent aucun rendu, utiles pour un état haute fréquence comme une position de curseur.

L'approche est détaillée dans la documentation de Zustand (opens in a new tab). C'est souvent le point d'équilibre pour une application interactive de taille moyenne : plus structuré qu'un empilement de Contexts, infiniment plus léger que Redux et sans le piège de re-render du premier. Le revers de cette souplesse : avec moins de conventions imposées, la discipline d'équipe est à tenir soi-même pour que le store ne devienne pas un fourre-tout.

Ce que chaque outil fait vraiment

CritèreContext APIRedux ToolkitZustand
Rôle réelDistribution d'une valeurConteneur d'état prévisibleMagasin global minimal
Abonnement granulaireNon, tout consommateur se re-rendOui, via sélecteursOui, via sélecteurs
Code à écrireFaibleÉlevéTrès faible
Provider requisOuiOuiNon
Outillage de debugAucun dédiéDevTools, voyage dans le tempsDevTools via middleware
Terrain idéalValeur globale stable, faible fréquenceGrande app, équipe, état complexeApp interactive moyenne, performance

Le piège de re-render, en détail

Le vrai départage entre ces outils tient à un mécanisme : comment un composant décide de se re-rendre quand l'état change. C'est ce qui sépare une interface qui reste fluide d'une interface qui saccade dès qu'elle grandit.

Avec le Context, il n'y a pas de décision : React re-rend tout consommateur dès que la valeur du Provider change, point. Le composant n'a aucun moyen de dire qu'il ne s'intéresse qu'à un seul champ. Sur une valeur qui change souvent, le nombre de re-renders inutiles croît avec le nombre de consommateurs.

Avec Redux et Zustand, le composant fournit un sélecteur, une fonction qui extrait la portion d'état qui l'intéresse. La bibliothèque compare le résultat du sélecteur entre deux mises à jour et ne re-rend que s'il a changé. Lire state.cart.count reste indolore quand state.theme bascule. C'est ce niveau d'abonnement, absent du Context, qui fait tenir la performance à mesure que l'état grossit.

Le sélecteur a sa propre embûche : la comparaison se fait par référence. Un sélecteur qui renvoie un nouveau tableau ou un nouvel objet à chaque appel, comme items.filter(...) ou { a, b }, échoue toujours la comparaison et re-rend à chaque mise à jour. La parade est connue : renvoyer des valeurs primitives quand c'est possible ou mémoïser la dérivation. Ce coût de re-render est le pendant, côté état, de la pipeline de rendu décortiquée dans optimiser le rendu (opens in a new tab).

Une méthode de décision en quelques questions

  • L'état ne concerne qu'un composant et ses enfants ? useState ou useReducer. On n'ajoute pas de dépendance pour ça.
  • C'est une donnée qui vient du serveur ? TanStack Query ou SWR, jamais un gestionnaire d'état client.
  • C'est une valeur globale stable et à faible fréquence comme le thème, la langue ou l'utilisateur ? Le Context API fait le travail sans dépendance supplémentaire.
  • C'est un état client global qui change au fil des interactions ? Zustand couvre la grande majorité des cas avec un minimum de code.
  • L'application est vaste, l'équipe nombreuse, l'état complexe et interdépendant, le besoin de traçabilité fort ? Redux Toolkit et sa discipline deviennent un investissement rentable.

Ces questions ne s'excluent pas : une même application combine souvent useState pour le local, TanStack Query pour le serveur et un seul store global pour le vrai état client partagé. Le piège n'est pas de choisir le mauvais outil unique, c'est de vouloir tout faire passer par un seul.

Les erreurs les plus fréquentes

  • Mettre l'état serveur dans le store global. On réimplémente mal un cache que TanStack Query ou SWR fournissent déjà. C'est la source numéro un de bugs de synchronisation et de code inutile.
  • Détourner le Context en gestionnaire haute fréquence. Sans abonnement granulaire, chaque changement re-rend tous les consommateurs. Un état qui bouge souvent appelle un store à sélecteurs, pas un Context.
  • Sortir Redux par réflexe sur une petite application. Cette lourdeur ne se rembourse que sur des projets d'une certaine taille. En dessous, elle ajoute de l'indirection sans bénéfice.
  • Renvoyer un nouvel objet depuis un sélecteur. La comparaison par référence échoue à chaque fois et le composant se re-rend quand même. Renvoyer des primitives ou mémoïser la dérivation.
  • Un store global unique partagé entre les requêtes en SSR. Sous Next.js, un store créé au niveau du module fuit d'un utilisateur à l'autre. Le store doit être instancié par requête pour isoler les états. C'est un cousin direct des fuites mémoire et cleanup patterns (opens in a new tab).
  • Tout centraliser dans un seul store. Un état local forcé dans le store global, c'est du couplage inutile et des re-renders élargis. Garder local ce qui est local.

Le bon choix dépend de l'état, pas de la mode

Un gestionnaire d'état range chaque donnée là où elle a du sens, ne re-rend que ce qui doit l'être et ne demande pas plus de code que le problème n'en réclame. Les débats Redux contre Zustand contre Context passent à côté de l'essentiel : ces outils ne répondent pas tous à la même question et le meilleur choix dépend du type d'état, de la taille du projet et de l'équipe.

Concrètement, un état bien géré, c'est des interactions qui répondent à l'instant, une interface qui reste fluide quand elle se complexifie et un code encore lisible six mois plus tard. C'est moins visible qu'une animation réussie mais c'est ce qui décide si elle tourne à 60 images par seconde ou si elle saccade au premier clic.