Accueil / Articles

Audit de useEffect : Dix erreurs courantes, leurs symptômes et les solutions réelles

Reconnaissez les dix erreurs les plus fréquentes avec useEffect en React, allant de l’absence de fonction de nettoyage et des closures obsolètes aux conditions de concurrence et à la synchronisation des props, puis appliquez la correction appropriée.

Éditeur : REACTAPP.TOP8 min de lecture1919 mots

Sources: 10 React useEffect Mistakes That Are Secretly Breaking Your App in 2026

React 19, le React Compiler et les composants serveur ont déplacé une grande partie du travail hors de useEffect, mais cet hook reste la source de nombreux bugs récurrents dans React : fuites mémoire, données obsolètes, clignotements et boucles de rendu. La plupart proviennent de dix erreurs fréquentes. Ci-dessous, chaque erreur est présentée avec sa version défectueuse, ses symptômes et une solution, suivie d’une liste de contrôle pour auditer vos propres effets.

Le principe derrière chaque solution : useEffect existe pour synchroniser un composant avec quelque chose en dehors de React, comme un chronomètre, une connexion socket ou une API du navigateur.

1. Oublier la fonction de nettoyage

Les chronomètres, les abonnements et les écouteurs d’événements créés dans un effet continuent de fonctionner après que le composant ait été rérendu ou démonté, à moins de les désactiver. Ici, l’intervalle n’est jamais annulé :

// ❌ Broken: No cleanup
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  // Missing: return () => clearInterval(timer);
}, []);

Les symptômes sont des intervalles qui fuient et des écouteurs dupliqués. En phase de développement, le mode strict monte, démonte et remonte délibérément les composants pour mettre cela en évidence : sans nettoyage, on obtient deux intervalles et le compteur avance deux fois par seconde. En production, la fuite apparaît chaque fois que le composant est remonté. Retournez une fonction qui annule la configuration :

// ✅ Fixed: Always clean up
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(timer);
}, []);

2. Utiliser un tableau de dépendances vide pour signifier « exécuter une fois »

Un tableau vide ne signifie pas « au montage » de manière particulière. Il indique à React que l’effet ne dépend de rien, donc il n’est jamais exécuté à nouveau. Si l’effet lit des props ou de l’état, ces valeurs restent figées à leurs valeurs de première rendu :

// ❌ Broken: Stale closure
function ChatRoom({ roomId }) {
  useEffect(() => {
    connectToRoom(roomId);
    return () => disconnectFromRoom(roomId);
  }, []); // roomId changes, but effect never re-runs
}

Lorsque roomId change, le composant reste dans la salle d’origine. Énumérez toutes les valeurs réactives utilisées par l’effet ; la règle de linting react-hooks/exhaustive-deps permet de détecter cela.

// ✅ Fixed: Include real dependencies
useEffect(() => {
  connectToRoom(roomId);
  return () => disconnectFromRoom(roomId);
}, [roomId]);

Désormais, React se déconnecte de la salle précédente et se reconnecte à la nouvelle chaque fois que roomId change.

3. Dépendre d’objets recréés à chaque rendu

Les dépendances sont comparées par référence. Un objet ou un littéral d’array créé pendant le rendu constitue une nouvelle référence à chaque fois, ce qui fait que l’effet qui en dépend se réexécute après chaque rendu :

// ❌ Broken: New object every render
function Parent() {
  const config = { timeout: 5000 }; // New reference every time

  useEffect(() => {
    fetchData(config);
  }, [config]); // Runs every single render
}

Stabilisez la référence. Si la valeur ne change jamais, définissez-la une seule fois en dehors du composant. Si elle est construite à partir de props ou d’état, enveloppez-la dans useMemo avec les bonnes dépendances. Le fragment ci-dessous montre ces deux options côte à côte ; en pratique, vous choisirez l’une d’elles :

// ✅ Fixed: Stable reference
const CONFIG = { timeout: 5000 }; // Defined outside component
function Parent() {
  const config = useMemo(() => ({ timeout: 5000 }), []);

  useEffect(() => {
    fetchData(config);
  }, [config]);
}

Souvent encore plus simple : dépendez de valeurs primitives (timeout plutôt que config) et construisez l’objet à l’intérieur de l’effet. Le compilateur React mémorise de nombreuses valeurs dans le code qu’il compile, mais pas les objets provenant de code non compilé ou de bibliothèques, donc ne comptez pas sur lui pour assurer la correction des effets.

4. Laisser les réponses asynchrones se concurrencer

Lorsqu’une dépendance change rapidement, comme c’est le cas avec une boîte de recherche ou un changement rapide d’onglet, plusieurs requêtes peuvent être en cours en même temps, et elles ne se résolvent pas nécessairement dans l’ordre :

// ❌ Broken: Race condition
useEffect(() => {
  fetch(`/api/search?q=${query}`).then(res => res.json()).then(setResults);
}, [query]);

Si un utilisateur tape « react » puis « react hooks », la réponse pour « react » peut arriver en dernier et écraser les résultats corrects. La solution consiste à annuler la requête obsolète lors du nettoyage. AbortController annule la requête, et l’erreur AbortError générée est ignorée afin qu’elle ne soit pas considérée comme une véritable défaillance :

// ✅ Fixed: Abort in-flight requests
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then(res => res.json())
    .then(setResults)
    .catch(err => {
      if (err.name !== 'AbortError') setError(err);
    });

  return () => controller.abort();
}, [query]);

Pour les interfaces de recherche en particulier, il existe d’autres subtilités abordées dans la correction des conditions de concurrence que le débouncing ne peut pas résoudre.

5. Stockage des valeurs dérivées dans l’état

Si une valeur peut être calculée à partir de props ou d’état existants, elle n’a pas besoin de son propre état ni d’un effet :

// ❌ Broken: Redundant state + effect
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Chaque modification s’affiche deux fois, d’abord avec une valeur obsolète de fullName, ce qui crée deux sources de vérité. Calculez la valeur lors de l’affichage, n’utilisant useMemo que pour des opérations réellement coûteuses :

// ✅ Fixed: Compute during render
const fullName = `${firstName} ${lastName}`;// Or for expensive computations
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);

Notez que l’extrait présente deux alternatives ; déclarer fullName deux fois dans le même contexte est une erreur de syntaxe, il faut donc n’en conserver qu’une. Le schéma général est abordé dans une alternative plus sûre pour synchroniser l’état avec useEffect.

6. Création d’une boucle de mise à jour

Un effet qui modifie l’état dépend également éternellement de son propre calendrier d’exécution :

// ❌ Broken: Infinite loop
useEffect(() => {
  setCount(count + 1);
}, [count]);

La mise à jour déclenche un rendu, le rendu modifie count, la valeur modifiée de count exécute à nouveau l’effet, et ainsi de suite. React finit par l’arrêter en affichant une erreur « Maximum update depth exceeded ». Une mise à jour fonctionnelle élimine la nécessité de lire count à l’intérieur de l’effet, ce qui signifie qu’il n’a plus besoin d’en être une dépendance :

// ✅ Fixed: Functional update + correct dependencies
useEffect(() => {
  const timer = setTimeout(() => {
    setCount(c => c + 1); // Functional update avoids dependency
  }, 1000);
  return () => clearTimeout(timer);
}, []); // No count dependency needed

Notez que cela modifie le comportement : le compteur s’incrémente désormais une fois, une seconde après son montage. Pour un compteur qui tourne en continu, utilisez setInterval avec une fonction de nettoyage, comme dans l’erreur 1. Toute appel à setState à l’intérieur d’un effet nécessite une condition qui garantisse son exécution finale.

7. Omettre complètement le tableau des dépendances

Sans deuxième argument, un effet s’exécute après chaque rendu. C’est parfois ce que l’on souhaite, mais en général c’est accidentel et coûteux :

// ❌ Broken: Runs on every render
useEffect(() => {
  console.log('Effect running');
  performHeavyComputation(data);
});

Le compilateur React réduit les re-render inutiles, mais il ne rend pas les opérations effectuées à l’intérieur des effets moins coûteuses ; par conséquent, un effet non protégé effectuant des tâches lourdes reste un goulot d’étranglement. Déclarez la véritable dépendance :

// ✅ Fixed: Explicit dependencies
useEffect(() => {
  performHeavyComputation(data);
}, [data]);

8. Gestion des événements utilisateur dans les effets

Les effets s’exécutent parce que quelque chose a été rendu, et non parce qu’un utilisateur a effectué une action. Les analyses liées à un indicateur d’état sont déclenchées chaque fois que cet indicateur change, pour n’importe quelle raison :

// ❌ Broken: Event logic in effect
useEffect(() => {
  if (isModalOpen) {
    trackEvent('modal_opened');
  }
}, [isModalOpen]);

Cet événement est enregistré lors du montage si le modal est déjà ouvert, lorsque un parent modifie une propriété, ou lors du rétablissement de l’état, et non seulement lorsque l’utilisateur clique. Placez la logique d’interaction dans le gestionnaire qui effectue cette interaction :

// ✅ Fixed: Event handler
function handleOpenModal() {
  setIsModalOpen(true);
  trackEvent('modal_opened');
}

Si vous pouvez nommer l’action de l’utilisateur qui doit déclencher le code, celle-ci doit être placée dans un gestionnaire.

9. Récupération de données avec un effet simple

Avec les composants serveur et les chargeurs de framework qui gèrent les données initiales dans de nombreuses applications, la récupération côté client via des effets est moins fréquente. Lorsque vous effectuez une récupération côté client, un effet brut est fragile :

// ❌ Broken: Raw fetch in effect
useEffect(() => {
  fetch('/api/user').then(r => r.json()).then(setUser);
}, []);

Il ne dispose d’aucun état de chargement, d’aucune gestion des erreurs, d’aucune déduplication des requêtes, d’aucun mécanisme de mise en cache et, comme le montre l’erreur 4, d’aucune protection contre les conflits concurrents. Une bibliothèque de récupération de données gère tout cela :

// ✅ Fixed: Use a data-fetching library
const { data: user, isLoading, error } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  staleTime: 5 * 60 * 1000,
});

TanStack Query et SWR sont les choix habituels. Sans eux, créez un hook personnalisé avec des fonctionnalités de gestion du chargement, des erreurs et des interruptions à la place d’effets de récupération improvisés.

10. Copier les props dans l’état et les synchroniser

Un composant qui reçoit une valeur initiale, la copie dans l’état puis utilise un effet pour refléter les modifications des props se contredit lui-même :

// ❌ Broken: Prop-to-state sync
function EmailInput({ initialEmail }) {
  const [email, setEmail] = useState(initialEmail);

  useEffect(() => {
    setEmail(initialEmail); // Sync prop to state
  }, [initialEmail]);

  return <input value={email} onChange={e => setEmail(e.target.value)} />;
}

Dès que le parent fournit un nouveau initialEmail, les saisies de l’utilisateur sont silencieusement écrasées, après un rendu supplémentaire avec la valeur ancienne. Il faut choisir : rendre le composant entièrement contrôlé, ou le conserver non contrôlé et le réinitialiser délibérément en changeant son key, ce qui permet à React d’obtenir une nouvelle instance avec un état mis à jour :

// ✅ Fixed: Controlled or fully uncontrolled
// Option 1: Fully controlled — no internal state
function EmailInput({ email, onChange }) {
  return <input value={email} onChange={onChange} />;
}
// Option 2: Fully uncontrolled with key
function EmailInput({ initialEmail }) {
  const [email, setEmail] = useState(initialEmail);
  return <input value={email} onChange={e => setEmail(e.target.value)} />;
}
// Usage: Reset by changing key
<EmailInput key={userId} initialEmail={user.email} />

Comment React 19 change la situation

Les versions récentes de React rendent ces erreurs à la fois plus visibles et moins nécessaires :

  • Le double montage des effets en mode développement de Strict Mode révèle tôt les nettoyages manquants ou incorrects.
  • Le compilateur React mémorise automatiquement les composants et de nombreuses valeurs, ce qui réduit les rendus accidentels mais ne corrige pas la logique des effets.
  • Les composants serveur s’occupent de la majeure partie du chargement initial des données, réservant les effets principalement aux tâches côté client telles que les abonnements, les animations et les API du navigateur.
  • L’API use peut lire des promesses et le contexte pendant le rendu, éliminant ainsi certains motifs d’utiliser des effets pour gérer des valeurs asynchrones.
  • Liste de vérification avant fusion pour les effets

    Au préalable de la mise en production d’un effet, vérifiez :

    • Chaque abonnement, horloge et écouteur dispose d’une procédure de nettoyage correspondante.
    • L’array des dépendances liste toutes les valeurs réactives lues par l’effet.
    • Les dépendances de type objet et tableau sont stables, ou remplacées par des dépendances primitives.
    • Les tâches asynchrones sont annulées ou ignorées lorsque les dépendances changent.
    • Rien de ce qui est calculé dans l’effet ne peut être dérivé simplement pendant le rendu.
    • Aucune mise à jour d’état ne peut déclencher à nouveau le même effet sans condition de stoppage.
    • La logique déclenchée par l’utilisateur se trouve dans les gestionnaires d’événements.
    • La récupération des données utilise, si possible, un composant serveur, un chargeur de framework ou une bibliothèque de récupération de données.
    • Chaque élément d’état possède une seule source de vérité.

    En résumé

    Presque tous les bugs liés à useEffect proviennent de l’utilisation de ce hook à des fins autres que la synchronisation externe. Les valeurs dérivées doivent être gérées dans la fonction de rendu, les actions de l’utilisateur dans les gestionnaires d’événements, les données serveur dans une couche de données, et l’état réinitialisable derrière un key. Les abonnements restants nécessitent des dépendances précises ainsi qu’un mécanisme de nettoyage. Souvent, le meilleur résultat d’une vérification est la suppression d’un effet inutile.