Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 617 minIntermédiaire

useEffect : les effets de bord, et quand s'en passer

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • synchroniser un composant avec un système extérieur grâce à useEffect ;
  • écrire le tableau de dépendances et la fonction de nettoyage ;
  • expliquer le double appel des effets en mode strict ;
  • remplacer un effet inutile par un calcul, un gestionnaire ou une clé ;
  • utiliser useRef pour viser un élément du DOM.

3 exercices corrigés1 schéma

useEffect est le hook qui exécute du code après l'affichage d'un composant, pour le synchroniser avec un système extérieur à React : le titre de l'onglet, un minuteur, un abonnement à un événement du navigateur, une connexion. Il prend une fonction et un tableau de dépendances qui dit quand la relancer. Ce chapitre explique son fonctionnement, le nettoyage, le double appel du mode strict, et surtout les nombreux cas où il ne faut pas l'utiliser.

C'est quoi un effet de bord ?

Le rendu d'un composant doit être pur (chapitre 2) : il calcule du JSX, rien d'autre. Un effet de bord, c'est tout ce qui agit sur le monde extérieur : modifier document.title, démarrer un setInterval, écouter la touche Échap sur toute la page, ouvrir une connexion WebSocket, écrire dans localStorage, envoyer une requête réseau.

Ces actions ont deux places possibles :

  1. Dans un gestionnaire d'événement, quand elles sont la conséquence d'une action de l'utilisateur : il clique sur « Supprimer », on envoie la requête de suppression. C'est le cas le plus fréquent.
  2. Dans un effet, quand elles doivent avoir lieu parce que le composant est affiché : tant que la page « Mes fiches » est à l'écran, le titre de l'onglet doit être « Mes fiches ».

La syntaxe de useEffect

tsx
import { useEffect } from 'react';

export function PageFiches() {
  useEffect(() => {
    document.title = 'Mes fiches de frais | AppliFrais';
  }, []);

  return <h1>Mes fiches de frais</h1>;
}
  • Le premier argument est la fonction d'effet. React l'exécute après avoir affiché le composant à l'écran, jamais pendant le rendu.
  • Le second argument est le tableau de dépendances : la liste des valeurs utilisées par l'effet qui viennent du composant (props, état, valeurs calculées).
ÉcritureQuand l'effet s'exécute
useEffect(fn, [])Une fois, après le premier affichage (le montage)
useEffect(fn, [a, b])Après le premier affichage, puis après chaque rendu où a ou b a changé
useEffect(fn)Après chaque rendu : presque toujours une erreur

Quand l'effet utilise une prop, elle doit figurer dans les dépendances :

tsx
export function PageFiche({ mois }: { mois: string }) {
  useEffect(() => {
    document.title = `Fiche de frais – ${mois} | AppliFrais`;
  }, [mois]);   // si le mois affiché change, le titre suit
  …
}

Le nettoyage

Un effet qui démarre quelque chose doit pouvoir l'arrêter. La fonction d'effet peut renvoyer une fonction de nettoyage, que React appelle :

  • avant de relancer l'effet (parce qu'une dépendance a changé) ;
  • quand le composant disparaît de l'écran (le démontage).
tsx
import { useEffect, useState } from 'react';

// Affiche l'heure, mise à jour chaque seconde
export function Horloge() {
  const [heure, setHeure] = useState(() => new Date().toLocaleTimeString('fr-FR'));

  useEffect(() => {
    const minuteur = setInterval(() => {
      setHeure(new Date().toLocaleTimeString('fr-FR'));
    }, 1000);
    return () => clearInterval(minuteur);   // nettoyage : on arrête le minuteur
  }, []);

  return <p>Il est {heure}.</p>;
}

Sans le clearInterval, chaque affichage de l'horloge laisserait un minuteur tourner pour toujours, même après avoir quitté la page : une fuite de mémoire. (La valeur initiale () => new Date()… est une fonction : React ne l'appelle qu'au premier rendu.)

Même schéma pour un écouteur d'événement global, par exemple fermer un panneau avec la touche Échap :

tsx
useEffect(() => {
  function surTouche(e: KeyboardEvent) {
    if (e.key === 'Escape') onFermer();
  }
  window.addEventListener('keydown', surTouche);
  return () => window.removeEventListener('keydown', surTouche);
}, [onFermer]);
le tempsMontagele composant apparaîtrenduaffichage à l'écraneffetdémarre le minuteurUne dépendance changesinon, l'effet n'est pas relancérenduaffichage à l'écrannettoyagearrête l'ancien minuteureffetdémarre le nouveauDémontagele composant disparaîtnettoyagearrête le minuteurMode strict, en développement seulementAu montage, React enchaîne effet → nettoyage → effet pour vérifier que le nettoyagedéfait bien ce que l'effet a fait. Un effet correct donne le même résultat.
Un effet s'exécute après l'affichage ; son nettoyage passe avant chaque relance et au démontage

StrictMode et le double appel

En développement, StrictMode (posé dans main.tsx) monte, démonte puis remonte chaque composant au premier affichage. Tes effets s'exécutent donc deux fois de suite : effet, nettoyage, effet. Ce n'est pas un bogue de React : c'est un test volontaire. Si ton effet est correct, avec un nettoyage symétrique, ce double appel ne change rien à ce que voit l'utilisateur. S'il révèle un problème (deux minuteurs, deux abonnements, un compteur qui double), c'est que le nettoyage manque.

Tu n'as peut-être pas besoin d'un effet

La documentation de React consacre une page entière à ce sujet, You Might Not Need an Effect. Un effet sert à se synchroniser avec un système extérieur. Pour tout ce qui reste dans React (calculer une valeur, réagir à un clic), un effet est inutile, plus lent (un rendu de plus) et source de bogues. Les quatre erreurs les plus courantes :

1. Calculer une valeur à partir de l'état ou des props.

tsx
// INUTILE : un état et un effet pour une valeur calculable
const [total, setTotal] = useState(0);
useEffect(() => {
  setTotal(frais.reduce((s, f) => s + f.montant, 0));
}, [frais]);

// JUSTE : un calcul pendant le rendu
const total = frais.reduce((s, f) => s + f.montant, 0);

La version avec effet affiche d'abord un total faux (l'ancien), puis refait un rendu avec le bon. La version calculée est juste du premier coup.

2. Réagir à une action de l'utilisateur.

tsx
// INUTILE : un état « drapeau » et un effet qui le surveille
const [aEnvoyer, setAEnvoyer] = useState(false);
useEffect(() => {
  if (aEnvoyer) {
    envoyerFiche();
    setAEnvoyer(false);
  }
}, [aEnvoyer]);
<button onClick={() => setAEnvoyer(true)}>Clôturer</button>

// JUSTE : l'action se fait dans le gestionnaire
<button onClick={() => envoyerFiche()}>Clôturer</button>

Le clic est la cause de l'envoi : le code va là où le clic est traité.

3. Réinitialiser un état quand une prop change. Pour remettre à zéro le formulaire quand on passe d'une fiche à une autre, inutile de surveiller l'identifiant dans un effet : donne au composant une clé qui dépend de la fiche. Une nouvelle clé crée un nouveau composant, avec un état neuf.

tsx
<FormulaireFrais key={fiche.id} ficheId={fiche.id} />

4. Copier une prop dans l'état. const [mois, setMois] = useState(props.mois) ne suit pas les changements de props.mois (la valeur initiale ne sert qu'au premier rendu). Utilise directement props.mois.

SituationOutil
Une valeur se déduit d'autres valeursUn calcul pendant le rendu
Quelque chose doit se passer parce que l'utilisateur a agiLe gestionnaire d'événement
Un état doit repartir de zéro quand une prop changeUne key sur le composant
Synchroniser avec le navigateur ou un système extérieur tant que le composant est affichéuseEffect
Charger des données d'une APIuseEffect en dépannage (chapitre 7), une bibliothèque comme TanStack Query en vrai projet (chapitre 9)

La boucle infinie

tsx
const [fiches, setFiches] = useState<FicheResume[]>([]);

useEffect(() => {
  getFiches().then((donnees) => setFiches(donnees));
});   // pas de tableau de dépendances

L'effet s'exécute après chaque rendu ; il change l'état ; le changement provoque un rendu ; qui relance l'effet… L'onglet Réseau montre des centaines de requêtes par seconde. Même boucle avec une dépendance qui change à chaque rendu, comme un objet créé dans le composant :

tsx
const filtre = { etat: 'CR' };                 // un NOUVEL objet à chaque rendu
useEffect(() => { … setFiches(…) }, [filtre]); // toujours « différent » : relancé à chaque rendu

React compare les dépendances avec Object.is : deux objets créés séparément sont toujours différents, même avec le même contenu. Mets dans les dépendances des valeurs primitives (filtre.etat, une chaîne) ou crée l'objet dans l'effet.

useRef : garder une valeur sans redessiner, ou viser un élément

useRef crée une « boîte » { current: … } que React conserve entre les rendus. Contrairement à l'état, modifier ref.current ne provoque pas de rendu. Deux usages :

Accéder à un élément du DOM, par exemple pour lui donner le focus :

tsx
import { useEffect, useRef } from 'react';

export function ChampRecherche() {
  const champ = useRef<HTMLInputElement>(null);

  useEffect(() => {
    champ.current?.focus();   // le focus dans le champ dès l'affichage
  }, []);

  return (
    <>
      <label htmlFor="recherche">Rechercher un frais</label>
      <input id="recherche" ref={champ} type="search" />
    </>
  );
}

Après l'affichage, champ.current contient l'élément <input> réel. Le ?. évite une erreur s'il vaut encore null. Donner le focus est un des rares cas légitimes d'accès direct au DOM.

Mémoriser une valeur qui ne s'affiche pas, comme l'identifiant d'un minuteur qu'on veut pouvoir arrêter depuis un gestionnaire.

useEffectEvent : lire une valeur récente sans relancer l'effet

React 19.2 a ajouté useEffectEvent. Il sert quand un effet doit lire une valeur à jour sans dépendre d'elle. Exemple : un minuteur qui enregistre le brouillon toutes les 30 secondes doit envoyer la dernière saisie, mais le minuteur ne doit pas redémarrer à chaque frappe.

tsx
import { useEffect, useEffectEvent } from 'react';

function SauvegardeAuto({ brouillon }: { brouillon: string }) {
  const enregistrer = useEffectEvent(() => {
    localStorage.setItem('brouillon-frais', brouillon);   // lit toujours la dernière valeur
  });

  useEffect(() => {
    const minuteur = setInterval(() => enregistrer(), 30_000);
    return () => clearInterval(minuteur);
  }, []);   // le minuteur ne redémarre pas à chaque frappe

  return null;
}

Une fonction créée par useEffectEvent ne se met pas dans les dépendances et ne s'appelle que depuis un effet. C'est un outil de cas particulier : la plupart de tes effets n'en ont pas besoin.

Exercices

Exercice 1 sur 3 Effet ou pas effet ? Réussi À revoir

Pour chaque besoin, dis s'il faut un effet. Sinon, dis quoi utiliser.

  1. Afficher le nombre de frais dont le montant dépasse 25 €.
  2. Envoyer la fiche à l'API quand l'utilisateur clique sur « Clôturer ».
  3. Mettre le titre de l'onglet à « Fiche d'octobre 2026 » tant que la fiche est affichée.
  4. Écouter la touche Échap pour fermer une fenêtre modale.
  5. Vider le champ « libellé » quand on passe de la fiche 12 à la fiche 13.
  6. Enregistrer la préférence « mode sombre » dans localStorage chaque fois qu'elle change.
Voir la réponseMasquer la réponse de l'exercice 1
  1. Pas d'effet : un calcul pendant le rendu, frais.filter((f) => f.montant > 25).length.
  2. Pas d'effet : l'appel va dans le gestionnaire onClick.
  3. Effet : le titre de l'onglet est un système extérieur à React, à synchroniser tant que le composant est affiché (dépendance : le mois).
  4. Effet, avec nettoyage : addEventListener sur window, puis removeEventListener.
  5. Pas d'effet : key={fiche.id} sur le formulaire.
  6. Effet acceptable (synchroniser localStorage avec l'état), dépendance : la préférence. Le plus simple reste d'écrire dans localStorage dans le gestionnaire qui change la préférence, puisqu'un seul endroit la modifie.

Tu avais trouvé ?

Exercice 2 sur 3 Corriger un effet Réussi À revoir

Ce composant affiche un compte à rebours avant la date limite de saisie. Il a trois défauts. Lesquels ? Corrige-le.

tsx
function CompteARebours({ limite }: { limite: Date }) {
  const [reste, setReste] = useState(0);
  useEffect(() => {
    setInterval(() => {
      setReste(Math.round((limite.getTime() - Date.now()) / 1000));
    }, 1000);
  });
  return <p>Encore {reste} secondes</p>;
}
Voir la réponseMasquer la réponse de l'exercice 2
  1. Pas de tableau de dépendances : l'effet se relance après chaque rendu, donc chaque seconde (chaque setReste provoque un rendu). Les minuteurs s'empilent.
  2. Pas de nettoyage : aucun minuteur n'est jamais arrêté, même après avoir quitté la page.
  3. L'état initial vaut 0 : pendant la première seconde, le composant affiche « Encore 0 secondes », et une fois la date limite passée, le compte devient négatif. On calcule la valeur initiale dès le premier rendu (useState(calculer)) et on la borne à 0 (Math.max(0, …)).
tsx
function CompteARebours({ limite }: { limite: Date }) {
  const calculer = () => Math.max(0, Math.round((limite.getTime() - Date.now()) / 1000));
  const [reste, setReste] = useState(calculer);

  useEffect(() => {
    const minuteur = setInterval(() => {
      setReste(Math.max(0, Math.round((limite.getTime() - Date.now()) / 1000)));
    }, 1000);
    return () => clearInterval(minuteur);
  }, [limite]);

  return <p>Encore {reste} secondes</p>;
}

Une fois le tableau de dépendances ajouté, limite doit y figurer : sinon le minuteur garderait l'ancienne date limite.

Attention : si le parent crée la date à chaque rendu (limite={new Date(2026, 10, 10)}), l'objet change à chaque fois et l'effet redémarre en permanence. Passe plutôt un nombre (limite.getTime()) ou une chaîne, qui se comparent par valeur.

Tu avais trouvé ?

Exercice 3 sur 3 Supprimer un effet Réussi À revoir

Réécris ce composant sans effet et sans état superflu.

tsx
function Recap({ frais }: { frais: Frais[] }) {
  const [nb, setNb] = useState(0);
  const [plusGros, setPlusGros] = useState<Frais | null>(null);
  useEffect(() => {
    setNb(frais.length);
    setPlusGros(frais.length > 0 ? frais.reduce((a, b) => (b.montant > a.montant ? b : a)) : null);
  }, [frais]);
  return <p>{nb} frais{plusGros && `, le plus gros : ${plusGros.libelle}`}</p>;
}
Voir la réponseMasquer la réponse de l'exercice 3
tsx
function Recap({ frais }: { frais: Frais[] }) {
  const plusGros = frais.length > 0 ? frais.reduce((a, b) => (b.montant > a.montant ? b : a)) : null;
  return <p>{frais.length} frais{plusGros && `, le plus gros : ${plusGros.libelle}`}</p>;
}

Deux valeurs dérivées des props : aucun état, aucun effet. La nouvelle version est juste dès le premier rendu, alors que l'ancienne affichait d'abord « 0 frais ».

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Appeler une API REST en React avec fetchRevoir le sommaire du cours