05

Effect (useEffect)

Gérer les side-effects et le monde extérieur

Le concept : Les composants React doivent idéalement être "purs" (ils ne modifient rien en dehors de leur rendu visuel). Lorsqu'il faut interagir avec le monde extérieur (récupérer des données API, s'abonner à WebSocket, démarrer un timer), nous utilisons les Side-Effects (effets secondaires) grâce au hook useEffect. C'est le portail contrôlé de votre composant vers l'extérieur.

UI Eff API Base
💡 Effets de bord & cycle de vie

Les composants React doivent être purs (mêmes entrées → même sortie). Mais parfois, il faut interagir avec le monde extérieur : appel API, abonnement WebSocket, modification du titre de la page, timer... C'est ce qu'on appelle des effets de bord.

useEffect est le "sas de sécurité" qui isole ce code. Il s'exécute après que React a rendu le composant dans le DOM.

Tableau de dépendancesQuand l'effet se déclencheÉquivalent classe
[] (vide)Une seule fois, au montagecomponentDidMount
[a, b]Montage + quand a ou b changecomponentDidUpdate
AbsentAprès chaque rendu (⚠️ boucle infinie possible)
Return functionAvant démontage ou prochain effetcomponentWillUnmount

📌 Syntaxe et exemples fondamentaux

useEffect — Les 3 formes
import { useEffect } from 'react';

// 1. Une seule fois (montage)
useEffect(() => {
    document.title = "Bonjour !";
}, []);

// 2. À chaque changement de `query`
useEffect(() => {
    search(query);
}, [query]);

// 3. Avec cleanup (nettoyage)
useEffect(() => {
    const handler = () => console.log("scroll");
    window.addEventListener("scroll", handler);

    return () => window.removeEventListener("scroll", handler);
}, []);

🌐 Cas d'usage réels

Fetch API (bonne pratique)
function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        let ignore = false; // évite les race conditions
        setLoading(true);

        fetch(`/api/users/${userId}`)
            .then(r => r.json())
            .then(data => {
                if (!ignore) {
                    setUser(data);
                    setLoading(false);
                }
            });

        return () => { ignore = true; };
    }, [userId]); // refetch si userId change

    if (loading) return <Spinner />;
    return <h1>{user.name}</h1>;
}
Timer, localStorage, WebSocket
// Timer avec cleanup
useEffect(() => {
    const id = setInterval(() => {
        setTime(t => t + 1);
    }, 1000);
    return () => clearInterval(id);
}, []);

// Sync avec localStorage
useEffect(() => {
    localStorage.setItem('theme', theme);
}, [theme]);

// Connexion WebSocket
useEffect(() => {
    const ws = new WebSocket(url);
    ws.onmessage = (e) => setMessages(m => [...m, e.data]);
    return () => ws.close(); // Fermer à la déconnexion
}, [url]);

⚠️ Pièges courants

Anti-pattern : dépendances manquantes
// ❌ PIÈGE : "stale closure" (valeur périmée)
useEffect(() => {
    console.log(count); // count figé à sa valeur initiale !
}, []); // count est utilisé mais absent du tableau

// ✅ CORRECT
useEffect(() => {
    console.log(count);
}, [count]); // count est dans le tableau
Anti-pattern : async direct
// ❌ useEffect ne peut pas être async directement
useEffect(async () => {
    const data = await fetch("/api");
    // 🚨 Retourne une Promise, pas une cleanup fn
}, []);

// ✅ Définir une function async DANS l'effet
useEffect(() => {
    async function load() {
        const data = await fetch("/api");
        setData(await data.json());
    }
    load();
}, []);
💡 useLayoutEffect — Variante synchrone de useEffect. S'exécute avant que le navigateur repeigne l'écran. Utile pour lire/modifier des positions DOM sans flash visuel (ex: faire apparaître un tooltip au bon endroit). À utiliser rarement, préférez useEffect.
Démo Effect
En attente...