Corriger les boucles de rendu infinies dans React : pièges de l'IA
Apprenez à réparer les boucles de rendu infinies dans React : repérez les pièges du code généré par l'IA et suivez un workflow étape par étape pour stabiliser les mises à jour.

À quoi ressemble une boucle de rendu infinie
Une boucle de rendu infinie se produit lorsqu'un composant React continue de se rendre encore et encore sans jamais se stabiliser. Au lieu d'un cycle de rendu normal, quelque chose déclenche constamment une nouvelle mise à jour.
Les premiers signes sont souvent visibles par l'utilisateur : la page semble bloquée, les boutons ne répondent plus, les champs d'entrée ont du retard, ou l'interface clignote parce que l'état change plus vite que le navigateur ne peut afficher.
Symptômes techniques courants :
- Une superposition d'erreur rouge comme "Too many re-renders. React limits the number of renders to prevent an infinite loop"
- Pics d'utilisation CPU et l'onglet devient chaud ou lent
- La même requête réseau qui se lance en boucle
- Des logs dans la console qui s'impriment sans arrêt
- Une UI qui se réinitialise (par exemple, une modal qui se rouvre instantanément après sa fermeture)
Ces boucles sont plus qu'un problème de performance. Elles cassent la logique. Un formulaire ne finit jamais sa validation, les contrôles d'authentification basculent entre « connecté » et « déconnecté », et des effets qui devraient s'exécuter une fois tournent des centaines de fois. Cela crée souvent des bugs supplémentaires : événements analytics dupliqués, toasts répétés, ou appels d'API soumis à des limites de taux.
Un scénario courant : un tableau de bord se charge, relance une requête, met à jour l'état avec la réponse, puis relance la requête parce qu'un effet dépend d'une valeur qui change à chaque rendu. L'utilisateur voit un spinner qui ne s'arrête jamais et votre backend reçoit un flot de requêtes identiques.
Pourquoi le code React généré par l'IA boucle souvent
Les outils d'IA peuvent produire du React qui a l'air correct en démo, puis se désagrège avec des données réelles et de vrais utilisateurs. Une grande raison : la confusion entre trois choses qui devraient rester séparées :
- l'état source (ce que vous stockez)
- les valeurs dérivées (ce que vous calculez à partir des valeurs stockées)
- les effets de bord (ce que vous faites parce que quelque chose a changé)
Quand ces éléments sont mélangés, vous finissez par mettre à jour l'état pendant le rendu, ou par exécuter un effet qui modifie les mêmes valeurs qui font que l'effet se relance.
Motif 1 : « Tout faire dans un seul useEffect »
Les composants générés par l'IA regroupent souvent le fetch, le parsing, le filtrage et l'état UI dans un seul effet. L'effet s'exécute, appelle setState plusieurs fois, provoque un re-render, puis s'exécute à nouveau parce qu'une dépendance a changé.
La boucle ressemble généralement à : fetch -> setState -> re-render -> l'effet se relance -> fetch à nouveau.
Motif 2 : dépendances instables recréées à chaque rendu
Une autre cause fréquente est de mettre des valeurs dans le tableau de dépendances qui sont recréées à chaque rendu, même si elles semblent « identiques ». Exemples : objets/tableaux inline, fonctions inline, ou valeurs dérivées reconstruites à chaque rendu.
React compare les dépendances par référence, pas par contenu. Une nouvelle référence d'objet signifie « changé », donc l'effet s'exécute à nouveau.
Un scénario réaliste : un tableau de bord construit queryParams comme un objet à l'intérieur du composant et l'utilise dans useEffect pour fetcher. À chaque rendu, un nouvel objet est créé, donc l'effet se déclenche, fetch à nouveau, setState encore, et le tableau de bord ne se stabilise jamais.
Comment confirmer où commence la boucle
D'abord, confirmez que c'est bien une vraie boucle et pas juste « beaucoup de rendus ». Ajoutez un compteur en haut de la fonction du composant.
console.count('Component render')
Si le compteur augmente sans cesse même quand vous n'interagissez pas avec la page, la boucle est confirmée.
Ensuite, isolez le déclencheur. Désactivez temporairement les effets jusqu'à ce que la boucle s'arrête. Le moyen le plus rapide est de commenter les effets un par un (ou de retourner tôt dans eux) et de recharger après chaque changement. Quand la boucle s'arrête, le dernier changement pointe vers la source.
Un ordre pratique :
- blocs
useEffectqui appellentsetStateou écrivent dans le stockage - effets qui appellent des APIs ou s'abonnent à quelque chose
- état dérivé calculé ou synchronisé à chaque rendu
- providers de contexte et composants parents
React DevTools peut aussi aider. Activez le surlignage des mises à jour, puis observez quelle partie de l'UI clignote en permanence. Cela montre souvent si la boucle se situe dans le composant courant ou un niveau au-dessus.
Si l'onglet Network montre la même requête en boucle, la chaîne est souvent : render -> effet -> fetch -> setState -> render. Corriger la boucle empêche aussi les requêtes en double et les problèmes de rate-limit.
Workflow étape par étape pour arrêter la boucle
Traitez une boucle de re-render comme une réaction en chaîne. Une mise à jour en provoque une autre. Votre travail est de trouver le premier domino.
- Identifier la mise à jour qui arrive juste avant le rendu suivant.
Surveillez quel setter s'exécute (setUser, setItems, setLoading) et ce qui le déclenche. S'il y a plusieurs setters, commentez-les un à un pour voir lequel arrête le cycle.
- Assurez-vous de ne pas mettre à jour l'état pendant le rendu.
Une erreur courante est d'appeler un setter pendant que vous « calculez » des valeurs, ou dans un helper qui s'exécute lors de la construction du JSX. Les mises à jour d'état appartiennent aux gestionnaires d'événements, aux effets ou aux callbacks, pas au corps du rendu.
- Réduisez l'effet à sa vraie tâche.
Réduisez l'effet à la plus petite version qui reproduit toujours le bug. Notez ce dont il dépend réellement (props, state, et toutes valeurs externes lues). Les problèmes de dépendance se cachent souvent ici.
- Stabilisez les entrées et rendez les mises à jour idempotentes.
Quelques corrections qui arrêtent rapidement les boucles :
- Rendre les entrées instables stables (mémoïser callbacks/objets, ou les sortir du composant)
- Ne pas stocker des valeurs dérivées dans l'état sauf si nécessaire (calculez-les à partir des props/état, ou utilisez
useMemo) - Protéger les mises à jour pour n'appeler
setStateque lorsqu'une valeur a réellement changé - Ajouter des cleanup pour les abonnements, timers et requêtes en cours
Si un effet « synchronise » une valeur dans une autre, il doit être sûr de s'exécuter plusieurs fois. L'exécution répétée d'un effet ne devrait pas changer l'état à moins que ses entrées aient réellement changé.
Corriger les problèmes de dépendances useEffect
La plupart des boucles useEffect reviennent au même motif : l'effet met à jour l'état, et ce changement d'état fait que l'effet se relance.
Considérez chaque setState à l'intérieur d'un effet comme suspect jusqu'à ce que vous puissiez expliquer pourquoi il s'arrête.
Règle clé : ne pas appeler setState sans condition dans un effet. Si l'effet s'exécute au mount et aux changements de dépendances, il faut une condition qui empêche une mise à jour répétée.
Une garde pratique : « mettre à jour seulement quand la prochaine valeur est réellement différente ». Cela compte quand le code reconstruit des tableaux ou objets et les stocke dans l'état à chaque fois, même si le contenu n'a pas changé.
Bonnes corrections :
- Comparer avant d'appeler
setState(ou comparer dans la mise à jour fonctionnelle) - Mémoïser les dépendances quand vous devez vraiment dépendre d'objets/fonctions
- Calculer les valeurs dérivées pendant le rendu au lieu de les synchroniser via un effet
- Garder les dépendances intentionnelles (dépendre de primitives quand possible)
Aussi, ne traitez pas le tableau de dépendances comme une checklist. Les linters sont utiles, mais ajouter une dépendance pour faire taire un avertissement peut transformer un effet d'initialisation en boucle auto-déclenchée. La vraie solution est souvent une restructuration : diviser un effet en deux plus petits, ou déplacer du travail dans un gestionnaire d'événements.
Mises à jour d'état qui déclenchent accidentellement des re-renders
Il est facile de se focaliser sur useEffect, mais certaines boucles proviennent d'erreurs simples d'état.
Appeler setState pendant le rendu
Si vous appelez setState dans le corps du rendu, React n'a d'autre choix que de rendre à nouveau. Cela peut être direct (un simple setX(...)) ou indirect (un helper appelé pendant le rendu qui met à jour l'état). Même quelque chose qui semble inoffensif, comme « normaliser des données si elles manquent », peut devenir une boucle.
État miroir (état dérivé qui poursuit les props)
Un autre piège est de copier des props dans l'état puis de « synchroniser » quand elles ne correspondent pas. Si la prop est un nouvel objet à chaque rendu, votre logique de synchronisation ne s'arrête jamais.
Quelques patterns qui causent souvent des rendus répétés :
- Mettre à jour l'état pendant le rendu (y compris dans des helpers appelés depuis le JSX)
- Stocker des valeurs dérivées dans l'état au lieu de les calculer
- Créer de nouveaux tableaux/objets et les assigner à l'état à chaque rendu sans tester l'égalité
- Passer une prop
keychangeante qui force des remounts
Quand le nouvel état dépend de l'état précédent, utilisez les mises à jour fonctionnelles. Au lieu de setCount(count + 1), utilisez setCount(c => c + 1). Cela évite des valeurs périmées qui peuvent déclencher des mises à jour « correctrices ».
Si vous avez plusieurs mises à jour liées qui se renvoient la balle (loading, erreurs, retries, données en cache), useReducer peut aider en centralisant les transitions.
Pièges liés aux fetchs, abonnements et cleanup
Les boucles se cachent souvent dans des effets « normaux » : fetchs, abonnements et timers. Si chaque callback appelle setState, vous pouvez vous retrouver avec des re-renders constants même si l'UI semble inchangée.
Fetchs qui se relancent
Si un fetch dépend d'un état que le fetch met lui-même à jour, vous obtenez une boucle.
Rendez les requêtes annulables pour que des réponses obsolètes n'écrasent pas un état plus récent. Utilisez AbortController dans l'effet et annulez dans le cleanup.
Pour réduire les doublons, ajoutez une garde simple avec une ref (pas de l'état), par exemple en suivant une clé de requête en cours.
Abonnements, timers et nettoyage
Les listeners et timers peuvent s'exécuter indéfiniment si vous oubliez le cleanup. Règle simple : chaque « démarrage » doit avoir un « arrêt » correspondant dans la fonction de cleanup.
Nettoyez les intervals/timeouts, désabonnez-vous des listeners et retirez les gestionnaires d'événements.
Une source de données, un seul écrivain
Évitez de mettre à jour la même donnée d'état depuis plusieurs endroits. Si un fetch écrit profile, un abonnement écrit aussi profile, et un autre effet « synchronise » profile, vous avez créé une boucle de rétroaction. Choisissez un seul propriétaire des écritures. Les autres peuvent déclencher un rafraîchissement, pas réécrire le même état.
Erreurs courantes qui gardent la boucle vivante
Certaines boucles « disparaissent » quand vous commentez une ligne, puis reviennent quand vous la remettez. Ça signifie souvent que le déclencheur sous-jacent est toujours présent.
StrictMode met en évidence les effets dangereux
Si un effet s'exécute deux fois en développement, React StrictMode fait son travail. Ne désactivez pas StrictMode pour masquer le problème. Rendez l'effet sûr à exécuter plusieurs fois en ajoutant cleanup, en protégeant les mises à jour ou en déplaçant l'initialisation hors de l'effet.
Closures périmées créent des mises à jour « correctrices »
Un pattern courant est un effet qui lit un état ancien, puis le « corrige » avec setState. Cette correction provoque un nouveau rendu, ce qui crée une autre lecture périmée, et le cycle se répète.
Si un effet utilise l'état mais que la liste de dépendances ne correspond pas, vous pouvez vous retrouver à vous battre contre vos propres valeurs passées. Préférez les mises à jour fonctionnelles quand vous avez besoin de la dernière valeur.
La mémoïsation peut aussi se retourner contre vous. useCallback/useMemo avec de mauvaises dépendances crée toujours une nouvelle fonction/objet chaque rendu. Si cette valeur est dans un tableau de dépendances, votre effet s'exécutera à chaque fois.
Façons rapides de repérer une boucle encore active :
- Logger les dépendances « stables » (fonctions, objets, tableaux) et vérifier si elles changent à chaque rendu
- Calculer les objets dérivés à l'intérieur de l'effet à partir de dépendances primitives
- S'assurer que chaque abonnement/listener a son cleanup
- Éviter de synchroniser l'état aux props sauf si c'est vraiment nécessaire
Checklist rapide avant de livrer
Faites une dernière passe avec le comportement réel des utilisateurs en tête. Les boucles disparaissent souvent sur le chemin heureux, puis reviennent quand les utilisateurs cliquent vite, changent d'onglet ou ont des réseaux lents.
- Cherchez des setters qui peuvent tourner pendant le rendu (y compris des helpers appelés depuis le JSX)
- Lisez chaque
useEffectcomme une phrase : « Quand X change, fais Y. » Assurez-vous qu'il y a une condition d'arrêt - Vérifiez la stabilité des dépendances (objets, tableaux et fonctions inline changent à chaque rendu)
- Vérifiez le cleanup pour timers, listeners, abonnements et observers
- Rendez le travail réseau résilient : annulez les requêtes obsolètes, dédupliquez les appels et ignorez les réponses tardives
Un test simple : ouvrez la page, puis changez un filtre trois fois rapidement. Si vous voyez des requêtes qui se chevauchent et l'UI qui se « corrige », vous avez probablement des dépendances instables et une annulation manquante.
Exemple réaliste : le tableau de bord qui refait des fetchs en boucle
Un cas courant dans les tableaux de bord administratifs générés : charger une liste d'utilisateurs, la stocker en état et afficher un tableau. Tout a l'air bien, sauf que la page relance constamment le fetch et l'UI tremble.
Le bug
Le pattern commence souvent avec un effet qui dépend d'un objet inline. Cet objet est recréé à chaque rendu, donc React le considère comme « changé » à chaque fois.
// Problem
function AdminUsers({ orgId }) {
const [users, setUsers] = React.useState([]);
const options = { method: "GET", headers: { "x-org": orgId } }; // new each render
React.useEffect(() => {
fetch("/api/users", options)
.then(r => r.json())
.then(data => setUsers(data));
}, [options]);
return <UsersTable users={users} />;
}
Certaines implémentations aggravent le problème en « normalisant » la réponse en un nouveau tableau à chaque fois, de sorte que setUsers est appelé même quand rien n'a changé.
La correction
Stabilisez les entrées de l'effet, évitez les mises à jour inutiles et annulez les requêtes en cours.
function AdminUsers({ orgId }) {
const [users, setUsers] = React.useState([]);
const options = React.useMemo(
() => ({ method: "GET", headers: { "x-org": orgId } }),
[orgId]
);
React.useEffect(() => {
const controller = new AbortController();
fetch("/api/users", { ...options, signal: controller.signal })
.then(r => r.json())
.then(data => {
setUsers(prev => (sameUsers(prev, data) ? prev : data));
})
.catch(err => {
if (err.name !== "AbortError") throw err;
});
return () => controller.abort();
}, [options]);
return <UsersTable users={users} />;
}
Pour vérifier que ça marche :
- Ajoutez un compteur de rendu et confirmez qu'il cesse de monter
- Regardez l'onglet Network : les requêtes répétées devraient s'arrêter
- Changez
orgIdune fois et confirmez que vous obtenez exactement un nouveau fetch
Un petit refactor aide à empêcher le retour du bug : extraire le fetch dans un hook useUsers(orgId), nommer les valeurs mémoïsées clairement et garder les dépendances d'effets courtes et stables.
Étapes suivantes si le code tourne encore en boucle
Si vous avez corrigé le problème évident (comme un tableau de dépendances manquant) et que l'app tourne encore, supposez qu'il y a plus d'un déclencheur. Beaucoup de boucles sont une chaîne : une mise à jour d'état déclenche un effet, cet effet met à jour autre chose, et un autre composant réagit en réécrivant le premier état.
Une petite correction suffit quand vous pouvez pointer une cause claire, comme un effet qui met l'état à chaque exécution ou un callback passé en prop qui change d'identité à chaque rendu.
Une réécriture est souvent préférable quand un composant fait trop de choses : fetch, tri, filtrage, état de formulaire et état UI tout mélangés. Si vous continuez à ajouter des drapeaux « n'exécutez qu'une fois », vous traitez les symptômes.
Si vous avez hérité d'une base de code React générée par l'IA qui ne se stabilise pas, FixMyMess (fixmymess.ai) peut aider en retraçant la chaîne de déclenchement et en réparant le flux d'état et d'effets sous-jacent, pas seulement en ajoutant des garde-fous. Un audit de code gratuit suffit souvent à pointer la boucle exacte et la correction sûre la plus rapide.