useEffect Boucle Infinie React : 5 Causes et Solutions

6 min de lecture React

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

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.