Ta console explose de logs, ton navigateur ralentit, l'onglet freeze — tu es face à une boucle infinie useEffect. C'est le bug React le plus frustrant pour les débutants. Voici les 5 causes et comment les corriger.
Comment détecter une boucle infinie
- Console qui spam : des centaines de logs identiques en quelques secondes
- Erreur React :
Too many re-renders. React limits the number of renders to prevent an infinite loop. - Navigateur qui rame : l'onglet consomme 100% CPU
- Network qui explose : des dizaines de requêtes fetch identiques
Cause 1 : Pas de tableau de dépendances
Sans [], useEffect s'exécute à chaque rendu. Si l'effet modifie le state → re-rendu → useEffect → re-rendu → boucle infinie.
// ❌ S'exécute à CHAQUE rendu
useEffect(() => {
fetch('/api/data')
.then(r => r.json())
.then(setData); // setState → re-rendu → useEffect → ...
}); // ← pas de []
// ✅ S'exécute une seule fois au montage
useEffect(() => {
fetch('/api/data')
.then(r => r.json())
.then(setData);
}, []); // ← tableau vide = une seule fois
Règle : mets TOUJOURS un tableau de dépendances. [] = au montage uniquement. [dep1, dep2] = quand ces valeurs changent.
Cause 2 : setState avec la valeur dans les deps
Tu mets à jour un state qui est dans les dépendances de useEffect. Changement → useEffect → changement → boucle.
// ❌ count change → useEffect → setCount → count change → ...
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, [count]); // count dans les deps + setCount(count) = 💥
// ✅ Utilise la forme fonctionnelle (pas besoin de count dans les deps)
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // prev, pas count
}, 1000);
return () => clearInterval(timer);
}, []); // [] car on n'utilise plus count directement
Cause 3 : Objet ou array dans les dépendances
Un nouvel objet {} est créé à chaque rendu. Même si le contenu est identique, la référence change → useEffect croit que la dépendance a changé.
// ❌ options est un NOUVEL objet à chaque rendu
function SearchResults({ query }) {
const options = { query, limit: 10 }; // Nouvelle référence à chaque rendu
useEffect(() => {
fetchResults(options);
}, [options]); // Référence change → boucle infinie
}
// ✅ Solution 1 : dépendre des valeurs primitives
useEffect(() => {
fetchResults({ query, limit: 10 });
}, [query]); // string = comparaison par valeur ✅
// ✅ Solution 2 : useMemo pour stabiliser la référence
const options = useMemo(() => ({ query, limit: 10 }), [query]);
useEffect(() => {
fetchResults(options);
}, [options]);
Cause 4 : Fonction qui recrée un objet
Tu appelles une fonction dans useEffect qui retourne un nouvel objet à chaque fois.
// ❌ getFilters() retourne un nouvel objet → boucle
function ProductList() {
const getFilters = () => ({ category: 'tech', inStock: true });
useEffect(() => {
fetch(`/api/products?${new URLSearchParams(getFilters())}`);
}, [getFilters()]); // Nouvel objet à chaque rendu = 💥
}
// ✅ Déplacer l'objet en dehors ou le mémoriser
const FILTERS = { category: 'tech', inStock: true }; // Constante
useEffect(() => {
fetch(`/api/products?${new URLSearchParams(FILTERS)}`);
}, []); // Pas de dépendance qui change
Cause 5 : Fetch → setState → re-render → fetch
Le cycle complet : le fetch est dans useEffect, il met à jour un state, le state déclenche un re-rendu, le re-rendu relance useEffect, qui refait le fetch...
// ❌ data change → re-rendu → useEffect → setData → data change → ...
const [data, setData] = useState(null);
const [url, setUrl] = useState('/api/data');
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(result => {
setData(result);
setUrl(result.nextPage); // ← Change url → relance useEffect
});
}, [url]); // url change à chaque fetch = 💥
// ✅ Séparer la pagination du fetch initial
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(setData);
}, [url]); // OK : url ne change que sur action utilisateur
const loadMore = () => setUrl(data.nextPage); // Déclenché par un clic
La règle d'or
Si tu peux calculer une valeur à partir du state ou des props, ne l'utilise PAS dans un useEffect.
// ❌ Anti-pattern : useEffect pour du state dérivé
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((s, i) => s + i.price, 0));
}, [items]); // Re-rendu inutile + risque de boucle
// ✅ Calcul direct pendant le rendu
const total = items.reduce((s, i) => s + i.price, 0);
// Pas de useEffect, pas de risque de boucle
useEffect sert à synchroniser avec un système extérieur (API, DOM, timer), pas à transformer du state. Pour tous les pièges : les 15 erreurs React à éviter.
FAQ
Comment savoir si mon useEffect boucle ?
Ajoute un console.log('useEffect called') au début de l'effet. Si tu vois des centaines de logs, c'est une boucle. React DevTools Profiler montre aussi les re-rendus excessifs.
Pourquoi useEffect s'exécute deux fois au montage ?
En mode développement, React StrictMode monte → démonte → remonte le composant pour détecter les problèmes. C'est normal et ne se produit pas en production. Ce n'est PAS une boucle infinie.
Est-ce que le React Compiler résout les boucles infinies ?
Non. Le React Compiler optimise les mémorisations (useMemo/useCallback) mais ne corrige pas les erreurs de logique dans useEffect. Comprendre les dépendances reste essentiel.
Pour approfondir les hooks : guide pratique useState et useEffect et les 23 questions React des débutants.
Envie de pratiquer ? GoGoKodo propose des ateliers interactifs pour apprendre React en codant directement dans le navigateur — 100% gratuit.