12k
All articles

Pourquoi votre useEffect s'exécute deux fois

Comprenez pourquoi React StrictMode exécute useEffect deux fois en développement et corrigez les requêtes, écouteurs et effets avec un nettoyage adapté.

OpenReplay Team
OpenReplay Team
Pourquoi votre useEffect s'exécute deux fois

En développement, le StrictMode de React exécute la phase de mise en place (setup) de chaque effet, puis son nettoyage (cleanup), puis à nouveau sa mise en place. Tout effet qui effectue un appel réseau, s’abonne à une source de données ou écrit dans la console se produit donc visiblement deux fois.

Vous écrivez un seul fetch dans un effet, vous ouvrez l’onglet Réseau, et vous y trouvez deux requêtes identiques ainsi que deux logs dans la console. On dirait un bug dans votre code, ou dans React.

Ce n’est ni l’un ni l’autre. Cette double exécution est un test délibéré de votre logique de nettoyage. Cet article explique ce que fait le StrictMode, comment corriger les trois types d’effets qui échouent le plus souvent à ce test, quels « correctifs » ne font que masquer le problème, et comment savoir que votre code est correct. Pour une remise à niveau sur le hook lui-même, commencez par ce guide du hook useEffect de React.

Points clés

  • Le StrictMode ajoute un cycle supplémentaire de mise en place et de nettoyage pour chaque effet, uniquement en développement, dans React 18 comme dans React 19.
  • Le StrictMode doit être activé explicitement, mais le template React de Vite enveloppe l’application dans <StrictMode>, et l’App Router de Next.js l’active par défaut.
  • Une fonction de nettoyage correcte annule exactement ce que la mise en place a fait : annuler la requête, retirer l’écouteur, effacer le minuteur ou fermer la connexion.
  • Voir deux requêtes dans l’onglet Réseau en développement est normal, même après une correction appropriée. L’important est que seule la réponse la plus récente puisse mettre à jour l’état.
  • Supprimer le StrictMode ou ajouter une garde useRef du type « déjà exécuté » masque l’absence de nettoyage sans la corriger.

Le symptôme : un fetch qui se déclenche deux fois

Un effet de récupération de données sans nettoyage est la façon la plus courante de découvrir ce comportement. Ce composant fonctionne correctement en production, mais se déclenche deux fois en développement :

import { useState, useEffect } from 'react';

function ProductDetails({ id }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    console.log('setup');
    fetch(`/api/products/${id}`)
      .then((res) => res.json())
      .then((data) => setProduct(data));
  }, [id]);

  return <h2>{product?.name}</h2>;
}

Vous obtenez deux logs setup et deux requêtes. Rien n’indique à React comment annuler la première requête, si bien que les deux vont jusqu’au bout.

Pourquoi useEffect s’exécute-t-il deux fois en développement ?

La documentation de référence du StrictMode explique que, lorsque le StrictMode est actif, React ajoute une passe supplémentaire de mise en place et de nettoyage à chaque effet pendant le développement. Votre effet exécute sa mise en place, puis son nettoyage, puis à nouveau sa mise en place, comme si le composant était monté, démonté puis immédiatement remonté.

Cette double exécution n’a lieu que dans les builds de développement. En production, un effet s’exécute une fois au montage du composant, puis uniquement lorsque ses dépendances changent ou que le composant est remonté.

Le StrictMode doit être activé explicitement : il ne s’applique qu’aux composants enveloppés dans <StrictMode>. Cela dit, de nombreux projets en bénéficient par défaut. Le fichier main.jsx du template React de Vite affiche <App /> à l’intérieur de <StrictMode>. L’App Router de Next.js active le StrictMode par défaut depuis Next.js 13.5.1. Les applications utilisant le Pages Router doivent l’activer avec reactStrictMode: true.

Ce cycle vérifie que votre nettoyage fonctionne réellement. Si un effet se comporte mal lorsque React enchaîne mise en place, nettoyage, mise en place, il se comportera aussi mal lorsqu’un utilisateur quittera la page puis y reviendra. Il se comportera également mal lorsque des modifications de code le réexécuteront en développement : la documentation Fast Refresh de Next.js indique que les effets doivent tolérer d’être réexécutés occasionnellement et précise que le StrictMode impose cette contrainte. Pour comprendre précisément quand le nettoyage intervient par rapport au rendu à l’écran (paint), consultez useEffect vs useLayoutEffect.

Comment corriger un useEffect qui s’exécute deux fois ?

Pour corriger un useEffect qui s’exécute deux fois, dotez-le d’une fonction de nettoyage qui annule ce que la mise en place a déclenché. Chaque défaillance courante a sa correction spécifique :

Ce que vous observez en devLe véritable problèmeCorrection
Deux fetchs, données potentiellement obsolètesRien n’annule la requête en coursAbortController ou un drapeau ignore dans le nettoyage
Un écouteur ou un abonnement se déclenche deux foisIl n’est jamais retiréLe retirer dans le nettoyage
Un compteur finit à 2La logique n’est pas un effet de bordLa déplacer dans un gestionnaire d’événements

Un fetch qui se déclenche deux fois

Un fetch qui se déclenche deux fois dans useEffect peut se corriger de deux façons. La première consiste à annuler la requête dans le nettoyage. Lorsqu’un fetch est annulé, il est rejeté avec une DOMException de type AbortError, que vous pouvez ignorer sans risque :

useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/products/${id}`, { signal: controller.signal })
    .then((res) => res.json())
    .then((data) => setProduct(data))
    .catch((err) => {
      if (err.name !== 'AbortError') throw err;
    });
  return () => controller.abort();
}, [id]);

L’autre option est le drapeau ignore, le pattern utilisé pour la récupération de données dans le guide React sur la synchronisation avec les effets :

useEffect(() => {
  let ignore = false;
  fetch(`/api/products/${id}`)
    .then((res) => res.json())
    .then((data) => {
      if (!ignore) setProduct(data);
    });
  return () => {
    ignore = true;
  };
}, [id]);

Quelle que soit la correction choisie, vous verrez toujours deux requêtes en développement. Avec AbortController, la première requête est annulée. Avec ignore, les deux requêtes aboutissent, mais seule la seconde est autorisée à mettre à jour l’état. En production, une seule requête est envoyée. Ce même nettoyage empêche également une réponse lente d’écraser une réponse plus récente lorsque id change.

Un écouteur qui fuit

Un écouteur d’événements ajouté dans useEffect provoque une fuite si le nettoyage ne le retire pas. Déclarez le gestionnaire à l’intérieur de l’effet, afin que le nettoyage retire exactement la même référence de fonction que celle ajoutée lors de la mise en place :

useEffect(() => {
  function handleResize() {
    setWidth(window.innerWidth);
  }
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

Un compteur qui s’incrémente deux fois

// Before: ends at 2 in development
useEffect(() => {
  setCount((c) => c + 1);
}, []);

La mise en place s’exécute deux fois en développement, donc l’incrémentation aussi. Aucun nettoyage ne peut « désincrémenter » le compteur, ce qui vous indique que cet effet ne devrait tout simplement pas exister. Placez l’incrémentation là où se produit l’événement déclencheur :

<button onClick={() => setCount((c) => c + 1)}>Add</button>

Faut-il supprimer le StrictMode ou ajouter une garde useRef ?

Non. Ces deux approches font disparaître la double exécution, mais laissent le bug sous-jacent en place.

Désactiver le StrictMode, que ce soit en supprimant l’élément englobant ou en définissant reactStrictMode: false dans votre configuration Next.js, supprime le test. Il manque toujours le nettoyage dans vos effets.

La garde useRef est l’option la plus tentante :

// Avoid: hides missing cleanup
const hasRun = useRef(false);

useEffect(() => {
  if (hasRun.current) return;
  hasRun.current = true;
  subscribe();
}, []);

La garde ignore la seconde mise en place en développement, et la console paraît propre. Mais un drapeau useRef qui saute la seconde exécution ne corrige pas l’absence de nettoyage. Il masque le problème en développement et laisse la fuite en place à chaque remontage réel. Une ref appartient à une seule instance de composant. Lorsqu’une route est démontée puis remontée en production, la nouvelle instance reçoit une nouvelle ref, et l’effet s’exécute donc à nouveau. Comme il n’y a toujours pas de nettoyage, l’ancien abonnement n’est jamais retiré.

Dans un enregistrement de session (session replay) d’une application dont les effets ne sont pas nettoyés, ce bug peut se manifester par des requêtes dupliquées ou des données obsolètes après une navigation avant/arrière. C’est exactement le bug que le StrictMode fait apparaître en développement.

Quand vous n’avez pas besoin d’un effet

Beaucoup d’effets qui se déclenchent deux fois n’auraient jamais dû être des effets. Les effets servent à synchroniser le composant avec quelque chose d’extérieur à React, du fait que ce composant est affiché à l’écran. Vous n’avez peut-être pas besoin d’un effet couvre les deux grandes catégories d’effets superflus.

Le travail déclenché par un événement a sa place dans les gestionnaires d’événements. Envoyer un formulaire, afficher une notification (toast) après un clic sur « Ajouter au panier » ou incrémenter un compteur se produit parce que l’utilisateur a fait quelque chose, et non parce qu’un composant s’est affiché.

Les valeurs calculables à partir des props ou de l’état ont leur place dans le rendu :

// Before: extra state, extra render, effect runs twice
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(first + ' ' + last);
}, [first, last]);

// After: computed during render
const fullName = first + ' ' + last;

Vérification rapide : votre effet est-il correct ?

Pour vérifier qu’un useEffect est correct, ajoutez un log à la fois dans la mise en place et dans le nettoyage :

useEffect(() => {
  console.log('setup');
  return () => console.log('cleanup');
}, []);

En développement, la console doit afficher :

setup
cleanup
setup

Un build de production n’affiche setup qu’une seule fois. Si vos logs affichent setup, cleanup, setup et que l’interface finit dans l’état attendu, l’effet fonctionne comme prévu et il n’y a rien à corriger.

Conclusion

Un useEffect qui s’exécute deux fois en développement, c’est le StrictMode qui vérifie que votre nettoyage fonctionne. Ne le faites pas taire. Passez en revue chaque effet qui se déclenche deux fois et faites en sorte qu’il réussisse ce test : annulez ou ignorez les requêtes obsolètes, retirez ce que vous ajoutez, et sortez complètement des effets la logique déclenchée par des événements ainsi que les valeurs dérivées. Une fois vos effets correctement nettoyés, ajoutez des error boundaries React pour qu’un composant qui lève une erreur pendant le rendu ne fasse pas planter toute la page.

FAQ

Pourquoi mon useEffect s'exécute-t-il deux fois en production, alors que le StrictMode est désactivé ?

En production, un effet s'exécute à nouveau lorsque la valeur de l'une de ses dépendances change ou lorsque le composant est remonté. Si une dépendance diffère de celle du rendu précédent, l'effet se réexécute : les objets et fonctions créés pendant le rendu comptent donc comme de nouvelles valeurs à chaque fois. Modifier la prop key, ou un rendu conditionnel qui retire puis réinsère le composant, provoque également un remontage et relance la mise en place.

Dois-je empêcher un événement analytics de se déclencher deux fois en développement ?

Non. La documentation React recommande de laisser l'appel analytics de visite de page dans son effet. Les utilisateurs ne peuvent pas savoir s'il s'est exécuté une ou deux fois, et un build de production n'envoie chaque visite qu'une seule fois. De toute façon, votre machine de développement ne devrait pas envoyer d'événements vers les métriques de production. Si vous devez déboguer ces événements, testez sur un build de staging fonctionnant en mode production, ou désactivez temporairement le StrictMode.

useLayoutEffect s'exécute-t-il aussi deux fois avec le StrictMode ?

Oui. La documentation de référence de useLayoutEffect décrit le même comportement en développement que pour useEffect : avec le StrictMode actif, React effectue d'abord une passe de mise en place et de nettoyage, puis la véritable mise en place. Les effets de layout nécessitent le même nettoyage symétrique, par exemple déconnecter un observer ou détruire l'instance d'un widget tiers. Passer à useLayoutEffect change le moment où l'effet s'exécute par rapport au rendu à l'écran, pas le nombre de fois où il s'exécute en développement.

Puis-je désactiver le StrictMode pour un seul composant tout en le conservant pour le reste de l'application ?

Non. Dès qu'une arborescence est enveloppée dans le StrictMode, chaque composant qu'elle contient est soumis aux vérifications, et aucun composant ne peut s'y soustraire individuellement. Vous pouvez en revanche placer l'élément StrictMode plus bas dans l'arborescence, afin qu'il ne couvre que certaines parties de l'application. Si le StrictMode n'enveloppe pas la racine, React omet l'exécution supplémentaire des effets lors du premier montage. Cette exécution ferait en effet se déclencher deux fois les effets des enfants alors que ceux de leurs parents ne se déclencheraient qu'une fois, une situation impossible en production.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.