Le Hook useEffect

Le hook useEffect est votre outil pour gérer les side effects dans les composants fonctionnels React.

Syntaxe de Base

javascript
import { useEffect } from 'react';

function Component() {
  useEffect(() => {
    // Code de l'effet
    console.log('Effect exécuté !');
  });

  return <div>Hello</div>;
}

Anatomie de useEffect

javascript
useEffect(callback, dependencies);
  • callback : La fonction contenant votre effet
  • dependencies : Tableau de dependances (optionnel mais important !)

Timeline d'Execution

Voici l'ordre d'execution complet d'un composant avec useEffect :

javascript
function Example() {
  console.log('1. Render : corps de la fonction');

  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('3. Effect : apres le rendu');

    return () => {
      console.log('4. Cleanup : avant le prochain effect ou unmount');
    };
  });

  console.log('2. Render : avant le return');

  return <div>Hello</div>;
}

// Ordre d'affichage :
// 1. Render : corps de la fonction
// 2. Render : avant le return
// 3. Effect : apres le rendu
// (si re-render)
// 1. Render : corps de la fonction
// 2. Render : avant le return
// 4. Cleanup : avant le prochain effect
// 3. Effect : apres le rendu

Dans quel ordre s'executent le rendu et l'effet ?

Les Trois Patterns de Dependances

Le tableau de dépendances contrôle quand l'effet s'exécute.

1. Sans Tableau : À Chaque Rendu

javascript
function Component() {
  const [count, setCount] = useState(0);

  // ❌ S'exécute après CHAQUE rendu
  useEffect(() => {
    console.log('Rendu !');
  });

  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

Attention

Rarement utilisé ! Peut causer des problèmes de performance.

Que se passe-t-il avec useEffect(() => { console.log('Hello') }) sans deuxième argument ?

2. Tableau Vide : Une Seule Fois (Mount)

javascript
function Component() {
  // ✅ S'exécute UNE FOIS au montage
  useEffect(() => {
    console.log('Composant monté !');
  }, []); // Tableau vide = une seule fois

  return <div>Hello</div>;
}

Cas d'usage :

  • Fetch initial de données
  • Initialisation de librairies externes
  • Abonnement à des events globaux
javascript
function DataLoader() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // Fetch une seule fois au chargement
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(data => setData(data));
  }, []); // Fetch uniquement au mount

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

3. Avec Dépendances : Quand les Dépendances Changent

javascript
function SearchResults() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);

  // ✅ S'exécute quand searchTerm change
  useEffect(() => {
    console.log(`Recherche pour : ${searchTerm}`);
    // Faire le fetch...
  }, [searchTerm]); // Re-exécute si searchTerm change

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {/* Afficher results */}
    </div>
  );
}

Règle d'Or

Mettez TOUTES les valeurs utilisées dans l'effet dans le tableau de dépendances.

javascript
// ❌ Mauvais : count manquant dans les dépendances
useEffect(() => {
  console.log(count);
}, []);

// ✅ Bon : count est dans les dépendances
useEffect(() => {
  console.log(count);
}, [count]);

React vous avertira dans la console si vous oubliez des dépendances !

Que fait useEffect(() => { fetch(url) }, []) ?

Fonction de Cleanup

Certains effets ont besoin d'être "nettoyés" avant le prochain effet ou quand le composant disparaît.

Syntaxe

javascript
useEffect(() => {
  // Setup de l'effet

  return () => {
    // Cleanup (nettoyage)
  };
}, [dependencies]);

Exemple : Timer

javascript
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // Setup : démarrer le timer
    const id = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup : arrêter le timer
    return () => {
      clearInterval(id);
      console.log('Timer nettoyé !');
    };
  }, []); // Une seule fois

  return <div>Secondes : {seconds}</div>;
}

Sans Cleanup

Si vous oubliez le cleanup, le timer continue même après la disparition du composant !

javascript
// ❌ Fuite mémoire : le timer continue indéfiniment
useEffect(() => {
  setInterval(() => {
    setSeconds(s => s + 1);
  }, 1000);
  // Pas de cleanup !
}, []);

Que retourne la fonction de cleanup dans useEffect ?

Exemple : Event Listener

javascript
function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    // Setup : ajouter le listener
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // Cleanup : retirer le listener
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Une seule fois

  return <div>Largeur : {width}px</div>;
}

Exemple : Abonnement

javascript
function ChatRoom({ roomId }) {
  useEffect(() => {
    // Setup : se connecter au chat
    const connection = createConnection(roomId);
    connection.connect();

    // Cleanup : se déconnecter
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // Re-connecter si roomId change

  return <div>Chat actif</div>;
}

Quand le Cleanup S'exécute ?

javascript
function Example({ userId }) {
  useEffect(() => {
    console.log('Effect : fetch user', userId);

    return () => {
      console.log('Cleanup : annuler fetch', userId);
    };
  }, [userId]);

  return <div>User {userId}</div>;
}

// Timeline :
// 1. userId = 1
//    → Effect : fetch user 1

// 2. userId = 2 (changement)
//    → Cleanup : annuler fetch 1
//    → Effect : fetch user 2

// 3. Composant unmount
//    → Cleanup : annuler fetch 2

Timeline du Cleanup

Le cleanup s'exécute :

  1. Avant le prochain effet (si les dépendances changent)
  2. Au unmount du composant

Jamais au premier mount !

Quand le cleanup s'exécute-t-il ?

Patterns Courants

Pattern 1 : Mettre à Jour le Titre

javascript
function PageTitle({ title }) {
  useEffect(() => {
    document.title = title;
  }, [title]);

  return <h1>{title}</h1>;
}

Pattern 2 : Logger les Changements

javascript
function DebugComponent({ value }) {
  useEffect(() => {
    console.log('Value changed:', value);
  }, [value]);

  return <div>{value}</div>;
}

Pattern 3 : Local Storage Sync

javascript
function FormWithPersistence() {
  const [name, setName] = useState('');

  // Charger depuis localStorage au mount
  useEffect(() => {
    const saved = localStorage.getItem('name');
    if (saved) {
      setName(saved);
    }
  }, []);

  // Sauvegarder à chaque changement
  useEffect(() => {
    localStorage.setItem('name', name);
  }, [name]);

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

Erreurs Courantes

Erreur 1 : Dépendances Manquantes

javascript
// ❌ Mauvais
function Bad() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(count); // count utilisé mais pas dans les dépendances !
  }, []);

  return <div>{count}</div>;
}

// ✅ Bon
function Good() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(count);
  }, [count]); // count dans les dépendances

  return <div>{count}</div>;
}

Erreur 2 : Boucle Infinie

javascript
// ❌ Boucle infinie !
function InfiniteLoop() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1); // setCount → re-render → useEffect → setCount → ...
  }, [count]);

  return <div>{count}</div>;
}

// ✅ Solution : pas de dépendance si vous voulez un seul increment
function Fixed() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(c => c + 1); // Une seule fois
  }, []);

  return <div>{count}</div>;
}

Erreur 3 : Oublier le Cleanup

javascript
// ❌ Fuite mémoire
function LeakyTimer() {
  useEffect(() => {
    setInterval(() => {
      console.log('Tick');
    }, 1000);
    // Pas de cleanup → le timer continue après unmount
  }, []);

  return <div>Timer</div>;
}

// ✅ Avec cleanup
function CleanTimer() {
  useEffect(() => {
    const id = setInterval(() => {
      console.log('Tick');
    }, 1000);

    return () => clearInterval(id); // Cleanup
  }, []);

  return <div>Timer</div>;
}

Erreur 4 : async/await Direct

javascript
// ❌ useEffect ne peut pas être async
useEffect(async () => {
  const data = await fetch('/api/data');
}, []);

// ✅ Créer une fonction async interne
useEffect(() => {
  async function fetchData() {
    const data = await fetch('/api/data');
  }

  fetchData();
}, []);

// ✅ Ou avec .then()
useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => console.log(data));
}, []);

Pourquoi pas async direct ?

useEffect doit retourner une fonction de cleanup (ou rien).

Si vous le rendez async, il retourne une Promise, ce qui n'est pas valide !

javascript
// ❌ Retourne une Promise
useEffect(async () => { ... });

// ✅ Retourne undefined ou une fonction
useEffect(() => {
  // ...
  return () => cleanup();
});

Pourquoi ne peut-on pas écrire useEffect(async () => { ... }) ?

Quand Utiliser useEffect ?

Cas d'UsageExempleDépendances
Mount uniquementFetch initial, init[]
Sur changementFetch quand ID change[id]
Sync avec props/stateUpdate document.title[title]
Timer/IntervalHorloge, countdown[] avec cleanup
Event listenerResize, scroll[] avec cleanup
AbonnementWebSocket, chat[roomId] avec cleanup

Comprendre, pas mémoriser

Ce qu'il faut retenir :

  • useEffect → pour les side effects (API, timers, DOM)
  • [] → une fois au mount
  • [value] → quand value change
  • Cleanup → retour d'une fonction pour nettoyer (timers, abonnements)
  • Pas async direct → créer une fonction async interne
  • Règle d'or → toutes les variables utilisées dans l'effet doivent être dans les dépendances

Avec la pratique, ces patterns deviendront naturels. Consultez la documentation React si vous avez un doute sur la syntaxe.

Prochaine Étape

Maintenant que vous maîtrisez useEffect, découvrons son usage le plus courant : récupérer des données depuis des APIs !