8 min

Gestion d'état React : rester basique dans les apps générées

Gestion d'état React simplifiée : séparez l'état serveur de l'état client, suivez quelques règles et repérez tôt les signes d'une complexité croissante.

Gestion d'état React : rester basique dans les apps générées

Ce qui tourne mal avec l'état dans de vraies apps React

L'état est toute donnée qui peut changer pendant l'exécution de votre app. Ça inclut ce que vous voyez (un modal ouvert), ce que vous éditez (un brouillon de formulaire) et les données que vous récupérez (une liste de projets). Le problème, c'est que tout ça s'appelle « état », alors qu'ils se comportent très différemment.

La plupart des apps qui partent en vrille suivent la même pente : trop de types d'état se mélangent au même endroit. Un composant finit par contenir des données serveur, des flags UI, des brouillons de formulaire et des valeurs dérivées, puis essaie de les synchroniser avec des effets. Rapidement, vous ne pouvez plus répondre à des questions simples comme « d'où vient cette valeur ? » ou « qu'est-ce qui la met à jour ?» sans fouiller dans plusieurs fichiers.

Les apps React générées y tombent plus vite parce qu'il est facile d'accepter la première version qui marche. Vous ajoutez un nouvel écran, copiez un pattern, corrigez un bug avec un autre useEffect, et voilà deux sources de vérité. Si le générateur ou l'équipe change de direction en cours de route (état local ici, store global là), la base de code accumule des patterns au lieu de s'appuyer sur un seul.

L'objectif est ennuyeux : moins de types d'état, moins d'endroits à inspecter. Quand il y a une maison évidente pour les données serveur et une maison évidente pour l'état purement UI, les bugs deviennent plus petits et les changements moins risqués.

« Rester ennuyeux » signifie respecter quelques règles :

  • Ne pas dupliquer les données serveur en état client sans raison claire.
  • Ne pas promouvoir l'état UI local en store global juste au cas où.
  • Ne pas transformer des valeurs dérivées en état propre sauf si leur calcul est réellement coûteux.

Un exemple concret : si une liste d'utilisateurs vient du backend, traitez-la comme server state et récupérez-la là où elle est utilisée. Si selectedUserId existe uniquement pour piloter un panneau de détails, gardez-la comme état UI proche de ce panneau. Mélanger les deux, c'est là que la complexité commence.

État serveur vs état client en clair

La plupart des problèmes d'état React viennent d'un mélange : traiter les données serveur comme de l'état UI. Séparez-les tôt et la gestion d'état reste calme, même quand l'app grandit.

L'état serveur appartient au backend : users, commandes, tâches, permissions, prix, feature flags. Il peut changer sans que votre app ne fasse quoi que ce soit (un autre onglet le met à jour, un admin le modifie, un job l'actualise, les données expirent). Parce que c'est partagé et changeant, il faut du fetch, du cache, du refetch et de la gestion d'erreurs.

L'état client, c'est ce qui n'intéresse que votre UI maintenant : quel modal est ouvert, quel onglet est sélectionné, un toggle de filtre, l'ordre de tri, une sidebar repliée, un brouillon de recherche. Si vous fermez l'onglet, il est normal de le perdre.

Un test rapide : « Puis-je rafraîchir la page et reconstruire ceci depuis le serveur ? »

  • Si oui, c'est probablement du server state.
  • Si non, c'est probablement du client state.

Il y a aussi l'état dérivé, qui évite de créer de l'état en plus. C'est une valeur que vous pouvez calculer à partir d'autres valeurs, donc vous ne la stockez pas. Listes filtrées, totaux, isFormValid et « afficher l'état vide » appartiennent généralement ici.

Exemple : vous récupérez une liste de projets (server state). Le filtre sélectionné et le flag du dialog « Nouveau projet » sont du client state. La liste visible après filtrage est de l'état dérivé. Si vous stockez la liste visible séparément, elle va se désynchroniser et vous passerez votre temps à chercher « pourquoi elle est obsolète ? ».

Cette séparation aide quand un outil comme Koder.ai génère des écrans rapidement : gardez les données backend dans une couche de fetch, gardez les choix UI près des composants et évitez de stocker des valeurs calculées.

Un ensemble de règles ennuyeuses qui prévient la plupart des problèmes

L'état devient pénible quand une donnée a deux propriétaires. Le moyen le plus rapide de garder les choses simples est de décider qui possède quoi et de s'y tenir.

  • Traitez les données API comme quelque chose que vous récupérez et mettez en cache, pas comme quelque chose que vous maintenez manuellement dans l'état du composant.
  • Ne clonez pas les données récupérées dans l'état local « au cas où ». Les copies créent de la dérive.
  • Gardez l'état client près de son lieu d'utilisation. Remontez-le seulement quand des parties séparées de l'arbre en ont vraiment besoin.
  • Stockez des IDs et de petits flags, pas des objets entiers. Re-derivez l'objet depuis le cache quand vous rendez.

Exemple : vous récupérez une liste d'utilisateurs et affichez les détails lorsqu'un est sélectionné. Une erreur classique est de stocker l'objet utilisateur complet en state. Stockez selectedUserId à la place. Gardez la liste dans le cache serveur. La vue de détails cherche l'utilisateur par ID, donc les refetchs mettent à jour l'UI sans code de sync en plus.

Dans les apps React générées, il est aussi facile d'accepter un état « utile » généré qui duplique les données serveur. Quand vous voyez du code qui fait fetch -> setState -> edit -> refetch, marquez une pause. C'est souvent le signe que vous êtes en train de construire une seconde base de données dans le navigateur.

Comment gérer l'état serveur sans trop y réfléchir

L'état serveur, c'est tout ce qui vit sur le backend : listes, pages de détail, résultats de recherche, permissions, compteurs. L'approche ennuyeuse consiste à choisir un seul outil et à s'y tenir. Pour beaucoup d'apps React, TanStack Query suffit.

Le but est simple : les composants demandent des données, affichent les états de chargement et d'erreur, et ne se soucient pas du nombre d'appels fetch sous le capot. Cela compte particulièrement dans les apps générées parce que les petites incohérences se multiplient vite quand on ajoute des écrans.

Traitez les query keys comme un système de nommage, pas comme un après-coup. Gardez-les cohérentes : keys stables en tableau, n'incluez que les inputs qui changent le résultat (filtres, page, tri), et préférez quelques formes prévisibles à beaucoup d'one-offs. Beaucoup d'équipes factorisent la construction des keys dans de petits helpers pour que chaque écran suive les mêmes règles.

Pour les écritures, utilisez des mutations avec un handling explicite du succès. Une mutation doit répondre à deux questions : qu'est-ce qui a changé, et que doit faire l'UI ensuite ?

Exemple : vous créez une nouvelle tâche. En cas de succès, soit vous invalidez la query de la liste des tâches (pour qu'elle se recharge), soit vous mettez à jour le cache de façon ciblée (ajouter la nouvelle tâche dans la liste en cache). Choisissez une approche par fonctionnalité et restez consistant.

Si vous êtes tenté d'ajouter des appels de refetch à plusieurs endroits « pour être sûr », choisissez plutôt un mouvement ennuyeux unique :

  • Invalidez la query exacte devenue obsolète.
  • Mettez à jour le cache pour la query exacte qui a changé.
  • Naviguez vers un écran qui récupère déjà la bonne query.

Patterns d'état client qui restent petits

L'état client est ce que le navigateur possède : un flag d'ouverture de sidebar, une ligne sélectionnée, le texte d'un filtre, un brouillon avant sauvegarde. Gardez-le proche de son utilisation et il reste généralement gérable.

Commencez petit : useState dans le composant le plus proche. Quand vous générez des écrans (par exemple avec Koder.ai), la tentation est de pousser tout dans un store global « au cas où ». C'est comme ça qu'on finit avec un store que personne ne comprend.

Règle simple de promotion

Ne remontez l'état que lorsque vous savez nommer le problème de partage.

  • Gardez l'état UI local par défaut.
  • Levez-le au parent commun le plus proche quand des composants frères en ont besoin.
  • Utilisez un petit store partagé seulement quand plusieurs routes ou composants distants ont besoin de la même valeur en même temps.

Exemple : un tableau avec un panneau de détails peut conserver selectedRowId dans le composant tableau. Si une toolbar ailleurs sur la page en a aussi besoin, remontez-le dans le composant page. Si une route distincte (comme l'édition en masse) en a besoin, là un petit store peut avoir du sens.

Formes de store lisibles

Si vous utilisez un store (Zustand ou similaire), gardez-le focalisé sur une seule mission. Stockez le « quoi » (IDs sélectionnés, filtres), pas les « résultats » (listes triées) que vous pouvez dériver.

Quand un store commence à grossir, demandez-vous : est-ce toujours une seule fonctionnalité ? Si la réponse honnête est « à peu près », scindez-le maintenant, avant que la prochaine feature ne le transforme en un nœud d'état que personne n'ose toucher.

Formulaires, brouillons et état UI temporaire

Devenez propriétaire du code généré
Récupérez le code source quand vous voulez le revoir, le refactorer ou l'étendre.

Les bugs de formulaire viennent souvent du mélange de trois choses : ce que l'utilisateur tape, ce que le serveur a sauvegardé, et ce que l'UI affiche.

Pour une gestion d'état ennuyeuse, traitez le formulaire comme état client jusqu'à la soumission. Les données serveur représentent la dernière version sauvegardée. Le formulaire est un brouillon. N'éditez pas l'objet serveur en place. Copiez les valeurs dans l'état brouillon, laissez l'utilisateur les modifier, puis soumettez et refetch (ou mettez à jour le cache) en cas de succès.

Décidez tôt ce qui doit persister quand l'utilisateur navigue ailleurs. Ce choix évite beaucoup de bugs surprises. Par exemple, le mode inline edit et les dropdowns ouverts devraient en général se réinitialiser, tandis qu'un brouillon de long wizard ou un message non envoyé peut persister. Persistez au reload seulement quand les utilisateurs l'attendent clairement (comme un checkout).

Conservez les règles de validation en un seul endroit. Si vous dispersiez les règles entre inputs, handlers de submit et helpers, vous finirez avec des erreurs incohérentes. Préférez un schéma unique (ou une fonction validate()), et laissez l'UI décider quand afficher les erreurs (on change, on blur ou on submit).

Exemple : vous générez un écran Edit Profile dans Koder.ai. Chargez le profil sauvegardé comme server state. Créez un état brouillon pour les champs du formulaire. Affichez « modifications non sauvegardées » en comparant brouillon vs sauvegardé. Si l'utilisateur annule, abandonnez le brouillon et montrez la version serveur. S'il sauvegarde, soumettez le brouillon, puis remplacez la version sauvegardée par la réponse serveur.

Étape par étape : simplifier une configuration d'état désordonnée

À mesure qu'une app générée grandit, il est courant de retrouver les mêmes données à trois endroits : état du composant, store global et cache. La solution n'est généralement pas une nouvelle librairie. C'est choisir une maison pour chaque morceau d'état.

Un flux de nettoyage qui marche dans la plupart des apps :

  1. Inventairez votre état. Listez ce que vous avez (listes, élément sélectionné, filtres, modals, brouillons) et étiquetez chaque élément comme server ou client.
  2. Supprimez les doublons. Retirez des états comme filteredUsers si vous pouvez le calculer depuis users + filter. Préférez selectedUserId à un objet selectedUser dupliqué.
  3. Centralisez le fetch dans une couche. Utilisez une seule approche server-state pour que le cache, le refetch et l'invalidation aient un seul cadre.
  4. Ajoutez un petit store client uniquement là où il le mérite (besoins UI cross-page comme un brouillon de wizard).
  5. Verrouillez des règles de nommage pour éviter que le bazar ne revienne.

Exemple : une app CRUD générée par Koder.ai commence souvent par un useEffect de fetch plus une copie de la liste dans un store global. Après centralisation du server state, la liste vient d'une seule query, et « rafraîchir » devient une invalidation plutôt que de la synchro manuelle.

Pour le nommage, restez cohérent et ennuyeux :

  • Queries : users.list, users.detail(id)
  • Client state : ui.isCreateModalOpen, filters.userSearch
  • Actions : openCreateModal(), setUserSearch(value)
  • Écritures serveur : users.create, users.update, users.delete

L'objectif est une source de vérité par chose, avec des frontières claires entre état serveur et état client.

Signes précoces que la complexité d'état est sur le point d'exploser

Construisez une base React ennuyeuse
Générez une app React depuis le chat, puis maintenez des frontières claires entre état serveur et UI dès le premier jour.

Les problèmes d'état commencent petit, puis un jour vous changez un champ et trois parties de l'UI ne sont pas d'accord sur la « vraie » valeur.

Le signe le plus clair est la duplication des données : le même user ou panier vit dans un composant, un store global et une cache de requête. Chaque copie se met à jour à un moment différent, et vous ajoutez du code juste pour les garder égales.

Un autre signe est le code de sync : des effets qui poussent l'état d'un côté à l'autre. Des patterns comme « quand la query change, mettez à jour le store » et « quand le store change, refetch » peuvent marcher jusqu'à ce qu'un cas limite produise des valeurs obsolètes ou des boucles.

Quelques drapeaux rouges rapides :

  • Vous ne pouvez pas expliquer « d'où vient cette valeur » et « qui la possède » en une phrase.
  • Votre store ou reducer importe des clients API ou connaît des codes HTTP.
  • Chaque nouvel écran ajoute des flags partagés comme needsRefresh, didInit, isSaving qui ne disparaissent jamais.
  • Vous écrivez des effets principalement pour refléter un état dans un autre.
  • Corriger un bug implique de mettre à jour le même champ à plusieurs endroits.

Exemple : vous générez un tableau de bord dans Koder.ai et ajoutez un modal Edit Profile. Si les données du profil sont stockées dans une query cache, copiées dans un store global et dupliquées dans un état local de formulaire, vous avez maintenant trois sources de vérité. La première fois que vous ajoutez du refetch en arrière-plan ou des updates optimistes, les incohérences apparaissent.

Quand vous voyez ces signes, le mouvement ennuyeux est de choisir un propriétaire unique pour chaque donnée et de supprimer les miroirs.

Pièges courants et comment les éviter

Stocker des choses « au cas où » est une des façons les plus rapides de rendre l'état pénible, surtout dans les apps générées.

Copier les réponses API dans un store global est un piège fréquent. Si les données viennent du serveur (listes, détails, profil utilisateur), ne les copiez pas par défaut dans un store client. Choisissez une maison pour les données serveur (habituellement le cache de requêtes). Utilisez le store client pour les valeurs UI que le serveur ignore.

Stocker des valeurs dérivées est un autre piège. Comptes, listes filtrées, totaux, canSubmit et isEmpty doivent généralement être calculés. Si les performances deviennent un vrai problème, mémoïsez, mais ne commencez pas par stocker le résultat.

Un méga-store unique pour tout (auth, modals, toasts, filtres, brouillons, flags d'onboarding) devient une poubelle. Séparez par frontières de fonctionnalité. Si l'état est utilisé par un seul écran, gardez-le local.

Context est idéal pour des valeurs stables (theme, current user id, locale). Pour des valeurs qui changent vite, il peut provoquer de larges rerenders. Utilisez Context pour le wiring, et l'état composant (ou un petit store) pour les valeurs UI à changements fréquents.

Enfin, évitez les noms incohérents. Des keys de query et des champs de store presque identiques créent des duplications subtiles. Choisissez une norme simple et respectez-la.

Checklist rapide avant d'ajouter plus d'état

Quand vous sentez l'envie d'ajouter « juste une variable d'état de plus », faites un contrôle rapide de propriété.

D'abord, pouvez-vous pointer vers un endroit unique où le fetch et le cache serveur se passent (un outil de query, un ensemble de keys) ? Si les mêmes données sont fetchées dans plusieurs composants et aussi copiées dans un store, vous payez déjà des intérêts.

Deuxièmement, cette valeur est-elle nécessaire seulement dans un écran (comme « le panneau de filtre ouvert ») ? Si oui, elle ne devrait pas être globale.

Troisièmement, pouvez-vous stocker un ID au lieu de dupliquer un objet ? Stockez selectedUserId et lisez l'utilisateur depuis votre cache ou liste.

Quatrièmement, est-ce dérivé ? Si vous pouvez le calculer à partir de l'état existant, ne le stockez pas.

Enfin, faites un test trace d'une minute. Si un coéquipier ne peut pas répondre en moins d'une minute à « d'où vient cette valeur ? » (prop, état local, cache serveur, URL, store), corrigez la propriété avant d'ajouter plus d'état.

Exemple : une app CRUD qui reste ennuyeuse en grandissant

Financez votre prochain build
Obtenez des crédits en partageant ce que vous créez ou en recommandant Koder.ai à d'autres.

Imaginez une app d'admin générée (par exemple via un prompt dans Koder.ai) avec trois écrans : liste clients, page détail client, et formulaire d'édition.

L'état reste calme quand chaque donnée a une maison évidente :

  • Server state : liste clients, fiche client, requêtes save/delete.
  • Client state : filtres de liste, ligne sélectionnée, brouillon d'édition.

La liste et les pages de détail lisent l'état serveur depuis un cache de requêtes. Quand vous sauvegardez, vous n'enregistrez pas les clients à nouveau dans un store global. Vous envoyez la mutation, puis laissez le cache se rafraîchir ou mettez à jour le cache.

Pour l'écran d'édition, gardez le brouillon local. Initialisez-le depuis le client récupéré, mais traitez-le séparément dès que l'utilisateur commence à taper. Ainsi, la vue détail peut se rafraîchir sans écraser des modifications en cours.

Une mise à jour optimiste ennuyeuse

L'UI optimiste est souvent l'endroit où les équipes dupliquent tout. Vous n'en avez généralement pas besoin.

Quand l'utilisateur clique sur Sauvegarder, mettez à jour uniquement l'enregistrement client en cache et l'élément correspondant dans la liste, puis revenez en arrière si la requête échoue. Gardez le brouillon dans le formulaire jusqu'à la réussite. S'il y a une erreur, affichez-la et conservez le brouillon pour que l'utilisateur puisse réessayer.

Quand une seconde fonctionnalité veut le même état

Supposons que vous ajoutez l'édition en masse et qu'elle a aussi besoin des lignes sélectionnées. Avant de créer un nouveau store, demandez-vous : cet état doit-il survivre à la navigation et au reload ?

  • Si oui, mettez-le dans l'URL (IDs sélectionnés, filtres).
  • Si non, gardez-le dans le composant de page.
  • Si plusieurs composants distants en ont vraiment besoin en même temps (toolbar + table + footer), introduisez un petit store partagé pour cet état client uniquement.

Prochaines étapes : gardez vos règles cohérentes dans les apps générées

Les écrans générés peuvent se multiplier vite, et c'est génial jusqu'à ce que chaque nouvel écran apporte ses propres décisions d'état.

Rédigez une courte note d'équipe dans le repo : ce qui compte comme server state, ce qui compte comme client state, et quel outil possède chaque chose. Gardez-la assez courte pour que les gens la suivent réellement.

Ajoutez une petite habitude en PR : étiquetez chaque nouvel état comme server ou client. Si c'est du server, demandez « où ça se charge, comment c'est mis en cache, et qu'est-ce qui l'invalide ? » Si c'est du client, demandez « qui le possède, et quand est-ce qu'il se réinitialise ? »

Si vous utilisez Koder.ai (koder.ai), Planning Mode peut vous aider à vous mettre d'accord sur les frontières d'état avant de générer de nouveaux écrans. Un snapshot et un rollback vous donnent un moyen sûr d'expérimenter quand un changement d'état tourne mal.

Choisissez une fonctionnalité (comme edit profile), appliquez les règles de bout en bout, et laissez cet exemple être celui que tout le monde copie.

FAQ

What’s the simplest way to untangle messy React state?

Commencez par étiqueter chaque morceau d'état comme server, client (UI) ou dérivé.

  • Server : données récupérées comme users, tasks, permissions.
  • Client : choix UI comme « modal ouvert », onglet sélectionné, texte de filtre.
  • Dérivé : valeurs qu'on peut calculer (liste filtrée, totaux, isValid).

Une fois étiquetés, assurez-vous que chaque item a un seul propriétaire évident (cache de requêtes, état local du composant, URL, ou un petit store).

How do I tell if something is server state or client state?

Utilisez ce test rapide : « Puis-je rafraîchir la page et reconstruire ceci depuis le serveur ? »

  • Si oui, traitez-le comme server state (fetch/cache/refetch).
  • Si non, traitez-le comme client state (état UI local, éventuellement un store).

Exemple : une liste de projets est du server state ; l'ID de la ligne sélectionnée est du client state.

Why is copying API data into local or global state such a big problem?

Parce que cela crée deux sources de vérité.

Si vous récupérez users puis les copiez dans useState ou un store global, vous devez alors les maintenir synchronisés pendant :

  • les refetchs en arrière-plan
  • les mises à jour depuis d'autres onglets/utilisateurs
  • les mises à jour partielles après des mutations

Règle par défaut : lisez les données serveur depuis un seul endroit (un cache de requêtes) et ne créez d'état local que pour les préoccupations UI ou les brouillons.

When is it okay to store derived values instead of computing them?

Stockez les valeurs dérivées seulement lorsque vous ne pouvez vraiment pas les calculer de manière peu coûteuse.

Généralement, calculez-les à partir des entrées existantes :

  • visibleUsers = users.filter(...)
  • total = items.reduce(...)
  • canSubmit = isValid && !isSaving

Si les performances deviennent un vrai problème (mesuré), préférez useMemo ou de meilleures structures de données avant d'introduire un état stocké qui peut devenir obsolète.

What’s a “boring” way to handle server state in React?

Par défaut : utilisez un outil d'état serveur (souvent TanStack Query) afin que les composants puissent simplement « demander des données » et gérer les états de chargement/erreur.

Principes pratiques :

  • Utilisez des query keys stables et cohérentes (n'incluez que les inputs qui changent le résultat).
  • Pour les écritures, utilisez des mutations et choisissez une stratégie par fonctionnalité :
    • invalider la query list/detail exacte, ou
    • mettre à jour le cache de façon ciblée.

Évitez de parsemer des appels refetch() partout « pour être sûr ».

When should I move UI state into a global store?

Gardez-le local jusqu'à ce que vous puissiez nommer un vrai besoin de partage.

Règle de promotion :

  • État local du composant par défaut.
  • Levez-le au plus proche parent commun quand des frères/sœurs en ont besoin.
  • Utilisez un petit store partagé seulement quand plusieurs composants/routes distants ont besoin de la même valeur en même temps.

Ainsi, votre store global n'a pas vocation à devenir une décharge pour des flags UI aléatoires.

Should I store the selected object or just its ID?

Stockez des IDs et de petits flags, pas des objets serveur complets.

Exemple :

  • selectedUserId
  • selectedUser (objet copié)

Ensuite, affichez les détails en recherchant l'utilisateur dans la liste ou le cache. Les refetchs en arrière-plan et les mises à jour se passent correctement sans code de synchronisation supplémentaire.

What’s the cleanest way to manage forms and drafts?

Considérez le formulaire comme un brouillon (client state) jusqu'à ce que vous soumettiez.

Patron pratique :

  • Récupérez l'enregistrement sauvegardé en tant que server state.
  • Initialisez un brouillon local à partir de celui-ci.
  • Laissez l'utilisateur modifier librement le brouillon.
  • À l'envoi, soumettez le brouillon puis rafraîchissez/mettre à jour le cache serveur.
  • À l'annulation, abandonnez le brouillon et affichez la version serveur.

Cela évite d'éditer l'objet serveur « en place » et de lutter contre les refetchs.

What are early warning signs that state complexity is about to explode?

Signes d'alerte courants :

  • Les mêmes données existent dans un composant, un store global et un cache de requêtes.
  • Des effets qui synchronisent l'état d'un côté et de l'autre ("quand la query change, set store", "quand le store change, refetch").
  • Des flags partagés qui s'accumulent (needsRefresh, didInit, isSaving).
  • Vous ne pouvez pas expliquer en une phrase « d'où vient cette valeur ».

La solution n'est généralement pas une nouvelle librairie : supprimez les miroirs et choisissez un unique propriétaire par valeur.

How do I keep state “boring” when using a generator like Koder.ai?

Les écrans générés peuvent rapidement diverger. Une garde-simple :

  • Server state : toujours via la même couche fetch/cache.
  • Client UI state : local par défaut, store uniquement si nécessaire.
  • Dérivés : calculés, pas stockés.

Si vous utilisez Koder.ai, utilisez Planning Mode pour décider des frontières avant de générer de nouveaux écrans, et appuyez-vous sur les snapshots/rollback pour revenir en arrière si un changement d'état tourne mal.

Related posts