React : useEffect et les effets de bord
Synchroniser un composant avec le monde extérieur (API, minuteurs, écouteurs) avec useEffect : le tableau de dépendances, le nettoyage, les pièges classiques, et pourquoi tu en as souvent moins besoin qu'il n'y paraît.
React : useEffect et les effets de bord
Le state et les événements suffisent à rendre un composant interactif en interne. Mais parfois, il doit dialoguer avec le monde extérieur : appeler une API, démarrer un minuteur, écouter la fenêtre. Ces actions sont des effets de bord, et React fournit le hook useEffect pour les gérer proprement.
# Qu'est-ce qu'un effet de bord ?
Le rendu d'un composant doit être pur : à partir des props et du state, il calcule du JSX, sans rien faire d'autre. Un effet de bord (side effect), c'est tout ce qui sort de ce cadre et touche au monde extérieur :
- appeler une API (
fetch) ; - démarrer un minuteur (
setInterval,setTimeout) ; - s'abonner à un événement (
window.addEventListener) ; - lire ou écrire dans
localStorage, manipuler le DOM directement.
Ces actions ne peuvent pas se faire pendant le rendu (elles le rendraient imprévisible). useEffect les exécute après, une fois l'affichage peint.
# La syntaxe
import { useEffect } from "react";
useEffect(() => {
// le code de l'effet, exécuté après le rendu
}, [dependances]);import { useEffect } from "react"; useEffect(() => { // le code de l'effet, exécuté après le rendu}, [dependances]);Deux parties : une fonction (l'effet) et un tableau de dépendances. C'est ce tableau qui pilote tout.
# Le tableau de dépendances : le cœur du hook
Il décide quand l'effet se relance :
| Tableau | L'effet se déclenche... |
|---|---|
[] | une seule fois, au montage du composant |
[valeur] | au montage + à chaque fois que valeur change |
| (omis) | après chaque rendu (rarement souhaité) |
Exemple typique - charger des données quand l'id change :
function Profil({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${id}`)
.then((r) => r.json())
.then(setUser);
}, [id]); // relance la requête si id change
if (!user) return <p>Chargement...</p>;
return <h1>{user.nom}</h1>;
}function Profil({ id }) { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${id}`) .then((r) => r.json()) .then(setUser); }, [id]); // relance la requête si id change if (!user) return <p>Chargement...</p>; return <h1>{user.nom}</h1>;}# Le nettoyage (cleanup)
Certains effets doivent être défaits : un minuteur à arrêter, un écouteur à retirer. L'effet peut retourner une fonction de nettoyage. React l'appelle avant de relancer l'effet, et quand le composant est démonté.
useEffect(() => {
const id = setInterval(() => console.log("tic"), 1000);
return () => clearInterval(id); // nettoyage : on arrête le minuteur
}, []);useEffect(() => { const id = setInterval(() => console.log("tic"), 1000); return () => clearInterval(id); // nettoyage : on arrête le minuteur}, []);Sans nettoyage, les minuteurs s'empilent et les écouteurs fuient : c'est la source n°1 des fuites mémoire en React.
# Les pièges classiques
- Dépendances manquantes → closure périmée. L'effet capture les valeurs du rendu où il a été créé (une closure). Si tu oublies une dépendance, il travaille avec des données obsolètes. Règle : mets dans le tableau toutes les valeurs (props, state) utilisées dans l'effet.
- Boucle infinie : appeler un setter de state dans un effet sans tableau de dépendances (ou avec une dépendance qui change à chaque rendu) → rendu → effet → setState → rendu → ... à l'infini.
- Effet trop fréquent : un tableau mal calibré relance un
fetchà chaque frappe. Vérifie toujours pourquoi et quand l'effet doit tourner.
Le linter React (react-hooks/exhaustive-deps) signale les dépendances manquantes. Ne le fais pas taire à l'aveugle : neuf fois sur dix, il a raison.
# Tu n'as (souvent) PAS besoin de useEffect
C'est l'erreur la plus répandue. useEffect sert à synchroniser avec un système extérieur - pas à réagir au flux normal de React. En particulier, tu n'en as pas besoin pour :
Transformer des données pour l'affichage → calcule-les pendant le rendu :
// ❌ Inutile : un effet + un state pour une valeur dérivée
const [nomComplet, setNomComplet] = useState("");
useEffect(() => { setNomComplet(prenom + " " + nom); }, [prenom, nom]);
// ✅ Une simple variable, calculée à chaque rendu
const nomComplet = prenom + " " + nom;// ❌ Inutile : un effet + un state pour une valeur dérivéeconst [nomComplet, setNomComplet] = useState("");useEffect(() => { setNomComplet(prenom + " " + nom); }, [prenom, nom]); // ✅ Une simple variable, calculée à chaque renduconst nomComplet = prenom + " " + nom;Réagir à une action utilisateur → mets la logique dans le gestionnaire d'événement, là où l'action se produit, pas dans un effet qui l'observe après coup.
Retiens la question : « est-ce que je me synchronise avec quelque chose d'extérieur à React ? ». Si non, ce n'est probablement pas un effet.
# Et en Next.js ?
Dans l'App Router, une grande partie des chargements de données se fait côté serveur, dans des Server Components qui await directement - sans useState ni useEffect. Le useEffect reste utile pour les effets réellement côté navigateur : minuteurs, écouteurs d'événements, accès aux API du navigateur. On détaillera cette frontière serveur / client dans une note dédiée.
# À retenir
- Un effet de bord touche au monde extérieur (API, minuteurs, écouteurs) ;
useEffectl'exécute après le rendu. - Le tableau de dépendances décide quand l'effet se relance :
[]= au montage,[x]= quandxchange. - Retourne une fonction de nettoyage pour tout ce qui doit être défait (minuteurs, abonnements), sous peine de fuite mémoire.
- Beaucoup d'effets sont inutiles : dériver une valeur se fait pendant le rendu, réagir à un clic se fait dans le gestionnaire d'événement.
Prochaine étape : gérer les saisies utilisateur pour de bon, avec les formulaires et les inputs contrôlés.