La gestion d'état est difficile car les apps jonglent entre plusieurs sources de vérité, des données asynchrones, des interactions UI et des compromis de performance. Découvrez des patterns pour réduire les bugs.

Dans une appli frontend, l'état est simplement les données dont dépend votre UI et qui peuvent changer au fil du temps.
Quand l'état change, l'écran doit se mettre à jour pour refléter ce changement. Si l'écran ne se met pas à jour, s'actualise de façon incohérente ou affiche un mélange d'anciennes et de nouvelles valeurs, vous ressentez immédiatement les « problèmes d'état » : des boutons qui restent désactivés, des totaux qui ne correspondent pas, ou une vue qui ne reflète pas l'action récente de l'utilisateur.
L'état apparaît dans des interactions petites ou grandes, comme :
Certains de ces éléments sont « temporaires » (comme un onglet sélectionné), d'autres semblent « importants » (comme un panier). Ce sont tous de l'état parce qu'ils influencent ce que l'UI rend à un instant donné.
Une variable simple n'importe où n'a d'importance que là où elle vit. L'état est différent parce qu'il a des règles :
Le vrai objectif de la gestion d'état n'est pas de stocker des données, mais de rendre les mises à jour prédictibles pour que l'UI reste cohérente. Si vous pouvez répondre à « qu'est-ce qui a changé, quand et pourquoi », l'état devient gérable. Sinon, même des fonctionnalités simples se transforment en surprises.
Au démarrage d'un projet frontend, l'état paraît presque ennuyeusement simple—dans le bon sens. Vous avez un composant, un champ, et une mise à jour évidente. Un utilisateur tape dans un champ, vous enregistrez la valeur, et l'UI se re-render. Tout est visible, immédiat et contenu.
Imaginez un champ texte unique qui affiche en aperçu ce que vous avez tapé :
Dans ce contexte, l'état est essentiellement : une variable qui change dans le temps. Vous pouvez pointer où elle est stockée et où elle est mise à jour, et c'est tout.
L'état local fonctionne parce que le modèle mental correspond à la structure du code :
Même avec un framework comme React, vous n'avez pas besoin de penser profondément à l'architecture. Les choix par défaut suffisent.
Dès que l'app cesse d'être « une page avec un widget » et devient « un produit », l'état ne vit plus en un seul endroit.
La même donnée peut maintenant être nécessaire à :
Un nom de profil peut apparaître dans l'entête, être modifié dans une page de paramètres, mis en cache pour un chargement plus rapide, et aussi servir à personnaliser un message de bienvenue. Soudain, la question n'est plus « comment stocker cette valeur ? » mais « où doit vivre cette valeur pour rester correcte partout ? »
La complexité de l'état ne croît pas progressivement avec les fonctionnalités—elle saute.
Ajouter un deuxième endroit qui lit la même donnée n'est pas « deux fois plus dur ». Cela introduit des problèmes de coordination : maintenir la cohérence des vues, éviter les valeurs obsolètes, décider qui met à jour quoi et gérer le timing. Une fois que vous avez quelques pièces d'état partagées plus du travail asynchrone, vous pouvez vous retrouver avec un comportement difficile à raisonner—alors que chaque fonctionnalité individuelle semble encore simple.
L'état devient pénible quand une même « vérité » est stockée à plusieurs endroits. Chaque copie peut diverger, et maintenant votre UI se dispute elle‑même.
La plupart des applications finissent par avoir plusieurs lieux pouvant contenir la « vérité » :
Chacun de ces endroits est un propriétaire valide pour une partie de l'état. Le problème commence quand ils veulent tous posséder la même donnée.
Un pattern courant : récupérer des données serveur, puis les copier dans l'état local « pour pouvoir les éditer ». Par exemple, vous chargez un profil utilisateur et faites formState = userFromApi. Plus tard, le serveur refait un fetch (ou un autre onglet met à jour l'enregistrement) et vous vous retrouvez avec deux versions : le cache dit une chose, votre formulaire en affiche une autre.
La duplication s'immisce aussi via des transformations « utiles » : stocker à la fois items et itemsCount, ou stocker selectedId et selectedItem.
Quand il y a plusieurs sources de vérité, les bugs ressemblent souvent à :
Pour chaque morceau d'état, choisissez un seul propriétaire—l'endroit où les mises à jour sont effectuées—et traitez tout le reste comme une projection (lecture seule, dérivée ou synchronisée dans une seule direction). Si vous ne pouvez pas pointer le propriétaire, vous stockez probablement la même vérité deux fois.
Beaucoup d'état frontend semble simple parce qu'il est synchrone : un utilisateur clique, vous définissez une valeur, l'UI se met à jour. Les effets de bord cassent cette histoire pas à pas.
Les effets de bord sont toutes les actions qui sortent du modèle pur « render basé sur des données » :
Chacun peut se déclencher plus tard, échouer de façon inattendue ou s'exécuter plusieurs fois.
Les mises à jour asynchrones introduisent le temps comme variable. Vous ne raisonnez plus en termes de « ce qui s'est passé », mais de « ce qui peut encore être en cours ». Deux requêtes peuvent se chevaucher. Une réponse lente peut arriver après une plus récente. Un composant peut être démonté alors qu'un callback asynchrone tente encore de mettre à jour l'état.
C'est pourquoi les bugs ressemblent souvent à :
Au lieu de parsemer des booléens comme isLoading partout, traitez le travail asynchrone comme une petite machine à états :
Suivez les données et le statut ensemble, et conservez un identifiant (comme un id de requête ou une clé de requête) pour pouvoir ignorer les réponses tardives. Cela rend la question « que doit afficher l'UI maintenant ? » une décision claire, pas une supposition.
Beaucoup de problèmes d'état commencent par une confusion simple : traiter « ce que l'utilisateur fait maintenant » de la même façon que « ce que le backend dit être vrai ». Les deux changent dans le temps, mais suivent des règles différentes.
L'état UI est temporaire et piloté par l'interaction. Il existe pour rendre l'écran comme l'utilisateur s'y attend à cet instant.
Exemples : modales ouvertes/fermées, filtres actifs, brouillon d'un champ de recherche, hover/focus, onglet sélectionné, et UI de pagination (page courante, taille de page, position de scroll).
Cet état est généralement local à une page ou à un arbre de composants. Il peut être réinitialisé lors d'une navigation.
L'état serveur est issu d'une API : profils utilisateurs, listes de produits, permissions, notifications, paramètres sauvegardés. C'est la « vérité distante » qui peut changer sans que votre UI ne fasse quoi que ce soit (quelqu'un d'autre modifie les données, le serveur recalcule, un job en arrière‑plan met à jour).
Parce qu'il est distant, il nécessite aussi des métadonnées : états de chargement/erreur, timestamps de cache, retries et invalidation.
Si vous stockez des brouillons UI à l'intérieur des données serveur, un refetch peut effacer les modifications locales. Si vous stockez des réponses serveur dans l'état UI sans règles de cache, vous vous battrez contre des données obsolètes, des fetchs en double et des écrans incohérents.
Un mode d'échec courant : l'utilisateur édite un formulaire pendant qu'un refetch de fond arrive et écrase le brouillon.
Gérez l'état serveur avec des patterns de cache (fetch, cache, invalider, refetch au focus) et traitez‑le comme partagé et asynchrone.
Gérez l'état UI avec des outils UI (état local de composant, context pour des préoccupations UI réellement partagées) et gardez les brouillons séparés jusqu'à ce que vous décidiez explicitement de les « sauvegarder » sur le serveur.
L'état dérivé est toute valeur que vous pouvez calculer à partir d'un autre état : un total de panier à partir des lignes, une liste filtrée à partir de la liste d'origine + la requête de recherche, ou un flag canSubmit issu des valeurs de champs et des règles de validation.
Il est tentant de stocker ces valeurs parce que c'est pratique (« je vais aussi garder total en state »). Mais dès que les entrées changent à plusieurs endroits, vous risquez la dérive : le total stocké ne correspond plus aux items, la liste filtrée ne reflète pas la requête actuelle, ou le bouton submit reste désactivé après correction d'une erreur. Ces bugs sont irritants parce que rien n'a l'air « faux » isolément—chaque variable d'état semble valide, simplement incohérente avec le reste.
Un pattern plus sûr : stocker la source minimale de vérité, et calculer le reste au moment de la lecture. En React, cela peut être une fonction simple ou un calcul mémoïsé.
const items = useCartItems();
const total = items.reduce((sum, item) =\u003e sum + item.price * item.qty, 0);
const filtered = products.filter(p =\u003e p.name.includes(query));
Dans des apps plus larges, des « selectors » (ou getters calculés) formalisent cette idée : un endroit définit comment dériver total, filteredProducts, visibleTodos, et chaque composant utilise la même logique.
Calculer à chaque rendu est généralement acceptable. Mettez en cache seulement si vous avez mesuré un coût réel : transformations coûteuses, listes gigantesques, ou valeurs dérivées partagées entre de nombreux composants. Utilisez la mémoïsation (useMemo, mémoïsation de selectors) de sorte que les clés de cache soient les vraies entrées—sinon vous revenez à la dérive, juste sous couvert d'optimisation.
L'état devient pénible quand il n'est pas clair qui possède la donnée.
Le propriétaire d'un morceau d'état est l'endroit de l'app qui a le droit de le mettre à jour. Les autres parties de l'UI peuvent le lire (via props, context, selectors, etc.), mais elles ne devraient pas le modifier directement.
Une propriété claire répond à deux questions :
Quand ces frontières s'estompent, vous obtenez des mises à jour conflictuelles, des moments « pourquoi ça a changé ? », et des composants difficiles à réutiliser.
Placer l'état dans un store global (ou un context de haut niveau) peut sembler propre : tout le monde y accède et vous évitez le prop drilling. Le compromis est un couplage involontaire—des écrans non liés dépendent des mêmes valeurs et de petits changements se répercutent dans l'app.
L'état global convient aux choses véritablement transversales, comme la session utilisateur courante, les feature flags applicatifs, ou une file de notifications partagée.
Un pattern courant est de commencer local et d'« élever » l'état au parent commun le plus proche seulement quand deux parties sœurs doivent se coordonner.
Si un seul composant a besoin de l'état, gardez‑le là. Si plusieurs composants en ont besoin, élevez‑le au plus petit propriétaire partagé. Si de nombreuses parties éloignées en ont besoin, alors envisagez le global.
Gardez l'état proche de l'endroit où il est utilisé sauf si le partage est requis.
Cela rend les composants plus faciles à comprendre, réduit les dépendances accidentelles, et facilite les refactorings futurs parce que moins de parties de l'app peuvent muter la même donnée.
Les apps frontend paraissent « mono‑fil », mais la saisie utilisateur, les timers, les animations et les requêtes réseau tournent indépendamment. Cela signifie que plusieurs mises à jour peuvent être en vol en même temps—et elles ne finissent pas nécessairement dans l'ordre où vous les avez lancées.
Une collision courante : deux parties de l'UI mettent à jour le même état.
query à chaque frappe.query (ou la même liste de résultats) quand il change.Individuellement, chaque mise à jour est correcte. Ensemble, elles peuvent s'écraser l'une l'autre selon le timing. Pire encore, vous pouvez afficher des résultats pour une requête précédente pendant que l'UI montre les nouveaux filtres.
Les conditions de course apparaissent quand vous lancez la requête A, puis rapidement la requête B—mais la requête A revient en dernier.
Exemple : l'utilisateur tape « c », « ca », « cat ». Si la requête « c » est lente et la requête « cat » rapide, l'UI peut afficher d'abord les résultats pour « cat » puis se faire écraser par les résultats obsolètes de « c » quand cette réponse plus ancienne arrive.
Le bug est subtil car tout « a fonctionné »—juste dans le mauvais ordre.
Vous voulez généralement l'une de ces stratégies :
AbortController).Une approche simple avec ID de requête :
let latestRequestId = 0;
async function fetchResults(query) {
const requestId = ++latestRequestId;
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
if (requestId !== latestRequestId) return; // stale response
setResults(data);
}
Les optimistes rendent l'UI instantanée : vous mettez à jour l'écran avant que le serveur confirme. Mais la concurrence peut briser les hypothèses :
Pour rendre l'optimisme sûr, il faut en général une règle de réconciliation claire : suivre l'action en attente, appliquer les réponses serveur dans l'ordre, et si vous devez revenir en arrière, revenir à un checkpoint connu (pas « à l'aspect actuel de l'UI »).
Les mises à jour d'état ne sont pas « gratuites ». Quand l'état change, l'app doit déterminer quelles parties de l'écran peuvent être affectées puis effectuer le travail pour refléter la nouvelle réalité : recalculs, re-renders, reformatage, et parfois re-fetch ou re-validation. Si cette chaîne de réaction est plus grosse que nécessaire, l'utilisateur le ressent comme latence, saccades ou boutons qui semblent « réfléchir » avant de répondre.
Un toggle unique peut déclencher beaucoup de travail supplémentaire :
Le résultat n'est pas que technique—c'est expérientiel : la frappe est retardée, les animations décrochent, et l'interface perd son côté « réactive » que les utilisateurs associent aux produits soignés.
Une des causes les plus fréquentes est un état trop large : un objet « panier global » contenant beaucoup d'informations non liées. Mettre à jour un champ fait paraître tout le bucket comme nouveau, donc plus d'UI se réveille que nécessaire.
Un autre piège est de stocker des valeurs calculées et de les mettre à jour manuellement. Cela crée souvent des mises à jour supplémentaires (et du travail UI en plus) juste pour garder tout synchronisé.
Divisez l'état en tranches plus petites. Séparez les préoccupations pour qu'un changement de champ de recherche ne rafraîchisse pas toute une page de résultats.
Normalisez les données. Au lieu de stocker plusieurs copies d'un même item, stockez‑le une fois et référencez‑le. Cela réduit les mises à jour répétées et évite les « tempêtes de changements » où une édition force la réécriture de nombreuses copies.
Mémoïsez les valeurs dérivées. Si une valeur se calcule à partir d'un autre état (comme des résultats filtrés), mettez en cache ce calcul pour qu'il ne se réexécute que quand les entrées changent vraiment.
Une bonne gestion d'état orientée perf vise surtout la containment : les mises à jour doivent affecter la plus petite zone possible, et le travail coûteux ne doit se faire que lorsque c'est vraiment nécessaire. Quand c'est le cas, les utilisateurs cessent de remarquer le framework et commencent à faire confiance à l'interface.
Les bugs d'état semblent souvent personnels : l'UI est « incorrecte », mais vous ne pouvez pas répondre à la question la plus simple—qui a changé cette valeur et quand ? Si un nombre bascule, une bannière disparaît ou un bouton se désactive, vous avez besoin d'une timeline, pas d'une supposition.
Le chemin le plus rapide vers la clarté est un flux de mise à jour prévisible. Que vous utilisiez des reducers, des événements ou un store, visez un pattern où :
setShippingMethod('express'), pas updateStuff)Un logging clair des actions transforme le débogage en « suivre le ticket » plutôt qu'en « fixer le regard sur l'écran ». Même de simples console.log (nom de l'action + champs clés) valent mieux que de reconstituer ce qui s'est passé à partir des symptômes.
N'essayez pas de tester chaque re-render. Testez plutôt les parties qui doivent se comporter comme de la logique pure :
Ce mélange attrape à la fois des « bugs mathématiques » et des problèmes de câblage réels.
Les soucis asynchrones se cachent dans les interstices. Ajoutez des métadonnées minimales qui rendent les timelines visibles :
Ainsi, quand une réponse tardive écrase une plus récente, vous pouvez le prouver immédiatement—et le corriger en toute confiance.
Choisir un outil d'état est plus simple si vous le considérez comme le résultat de décisions de design, pas comme point de départ. Avant de comparer des librairies, cartographiez vos frontières d'état : qu'est‑ce qui est purement local au composant, qu'est‑ce qui doit être partagé, et qu'est‑ce qui est réellement « données serveur » à récupérer et synchroniser.
Une façon pragmatique de décider est d'observer quelques contraintes :
Si vous commencez par « on utilise X partout », vous stockerez les mauvaises choses au mauvais endroit. Commencez par la propriété : qui met à jour cette valeur, qui la lit et que doit-il se passer quand elle change.
Beaucoup d'apps s'en tirent bien avec une librairie server-state pour les données API et une petite solution pour l'état UI (modales, filtres, brouillons de formulaires). L'objectif est la clarté : chaque type d'état vit là où il est le plus simple à raisonner.
Si vous itérez sur les frontières d'état et les flux asynchrones, Koder.ai peut accélérer la boucle « essayer, observer, affiner ». Comme il génère des frontends React (et des backends Go + PostgreSQL) à partir d'échanges avec un agent, vous pouvez prototyper des modèles de propriété alternatifs (local vs global, cache serveur vs brouillons UI) rapidement, puis garder la version qui reste prévisible.
Deux fonctionnalités pratiques aident lors d'expérimentations d'état : Planning Mode (pour esquisser le modèle d'état avant de construire) et snapshots + rollback (pour tester des refactors comme « supprimer l'état dérivé » ou « introduire des ID de requête » sans perdre une base fonctionnelle).
L'état devient plus simple quand vous le traitez comme un problème de design : décidez qui le possède, ce qu'il représente et comment il change. Utilisez cette checklist quand un composant commence à sembler « mystérieux ».
Demandez : Quelle partie de l'app est responsable de ces données ? Placez l'état le plus près possible de l'endroit où il est utilisé, et élevez‑le seulement quand plusieurs parties en ont réellement besoin.
Si vous pouvez calculer quelque chose à partir d'un autre état, ne le stockez pas.
items, filterText).visibleItems) lors du rendu ou via mémoïsation.Le travail asynchrone est plus clair quand vous le modélisez directement :
status: 'idle' | 'loading' | 'success' | 'error', plus data et error.isLoading, isFetching, isSaving, hasLoaded, …) au lieu d'un seul statut.Visez moins de bugs « comment est‑on arrivé dans cet état ? », des changements qui ne nécessitent pas de toucher cinq fichiers, et un modèle mental où vous pouvez pointer un endroit et dire : ici vit la vérité.