Chapitre 2 : useEffect
Ce chapitre est **crucial**. useEffect est le hook le plus complexe à maîtriser, mais aussi le plus utilisé après useState.
Qu'est-ce qu'un effet de bord ?
Un effet de bord (side effect) est toute opération qui affecte quelque chose en dehors du composant :
- Appeler une API
- Modifier le titre de la page
- S'abonner à un événement (WebSocket, resize, scroll)
- Manipuler le DOM directement
- Utiliser un timer (setTimeout, setInterval)
Ces opérations ne peuvent pas être faites directement dans le corps du composant, car celui-ci est exécuté à chaque rendu.
Syntaxe de base
- snippet.javascript
import { useEffect } from 'react'; function MyComponent() { useEffect(() => { // Code exécuté après le rendu console.log('Composant rendu !'); }); return <div>Mon composant</div>; }
Le tableau de dépendances
Le deuxième argument de useEffect contrôle quand l'effet s'exécute :
Sans tableau : à chaque rendu
- snippet.javascript
useEffect(() => { console.log('Exécuté après CHAQUE rendu'); });
Rarement utile - peut causer des problèmes de performance.
Tableau vide : une seule fois
- snippet.javascript
useEffect(() => { console.log('Exécuté UNE SEULE FOIS après le premier rendu'); }, []);
Équivalent à componentDidMount en classes.
Avec dépendances : quand elles changent
- snippet.javascript
useEffect(() => { console.log('Exécuté quand userId change'); fetchUser(userId); }, [userId]);
L'effet s'exécute :
- Après le premier rendu
- Après chaque rendu où
userIda changé
Exemple concret : appel API
- snippet.javascript
function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // Fonction async dans useEffect const fetchUser = async () => { setLoading(true); setError(null); try { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); setUser(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchUser(); }, [userId]); // Re-fetch quand userId change if (loading) return <p>Chargement...</p>; if (error) return <p>Erreur : {error}</p>; if (!user) return null; return <div>{user.name}</div>; }
Fonction de nettoyage (cleanup)
useEffect peut retourner une fonction qui sera appelée :
- Avant la prochaine exécution de l'effet
- Quand le composant est démonté
- snippet.javascript
useEffect(() => { // Setup const subscription = api.subscribe(data => { setData(data); }); // Cleanup return () => { subscription.unsubscribe(); }; }, []);
Exemple : événement de redimensionnement
- snippet.javascript
function WindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight }); useEffect(() => { const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener('resize', handleResize); // Cleanup : retirer l'écouteur return () => { window.removeEventListener('resize', handleResize); }; }, []); // [] car on veut s'abonner une seule fois return <p>{size.width} x {size.height}</p>; }
Exemple : timer
- snippet.javascript
function Timer() { const [seconds, setSeconds] = useState(0); useEffect(() => { const interval = setInterval(() => { setSeconds(s => s + 1); }, 1000); // Cleanup : arrêter le timer return () => { clearInterval(interval); }; }, []); return <p>{seconds} secondes</p>; }
Pièges courants
1. Boucle infinie
- snippet.javascript
// DANGER ! Boucle infinie useEffect(() => { setCount(count + 1); // Modifie l'état }); // Pas de tableau = exécuté à chaque rendu // Modifie l'état → re-rendu → useEffect → modifie l'état → ...
Solution : toujours mettre un tableau de dépendances.
2. Dépendances manquantes
- snippet.javascript
// PROBLÈME : userId n'est pas dans les dépendances useEffect(() => { fetchUser(userId); }, []); // Fetch uniquement au montage, ignore les changements de userId
Solution : ajouter toutes les valeurs utilisées dans le tableau.
- snippet.javascript
useEffect(() => { fetchUser(userId); }, [userId]); // Correct
3. Objet ou fonction dans les dépendances
- snippet.javascript
// PROBLÈME : l'objet est recréé à chaque rendu function MyComponent({ user }) { const options = { limit: 10 }; // Nouvel objet à chaque rendu useEffect(() => { fetchData(options); }, [options]); // options change à chaque rendu ! }
Solution : déplacer hors du composant ou utiliser useMemo (module suivant).
4. Async dans useEffect
- snippet.javascript
// INCORRECT - useEffect ne peut pas être async useEffect(async () => { // Erreur ! const data = await fetchData(); }, []); // CORRECT - définir une fonction async à l'intérieur useEffect(() => { const loadData = async () => { const data = await fetchData(); setData(data); }; loadData(); }, []);
Ordre d'exécution
- snippet.javascript
function MyComponent() { console.log('1. Rendu'); useEffect(() => { console.log('3. Effet'); return () => console.log('2. Cleanup (si re-rendu)'); }); return <div>Test</div>; } // Premier rendu : // 1. Rendu // 3. Effet // Re-rendu : // 1. Rendu // 2. Cleanup // 3. Effet
Comparaison avec le cycle de vie PHP
| Événement | PHP | React useEffect |
|---|---|---|
| Initialisation | constructeur | useEffect(() => {}, []) |
| À chaque requête | code du contrôleur | useEffect(() => {}) |
| Nettoyage | destructeur | fonction retournée par useEffect |
Quand utiliser useEffect ?
OUI :
- Appels API
- Abonnements (WebSocket, events)
- Timers
- Modifier le DOM directement
- Synchroniser avec un système externe
NON :
- Calculer une valeur dérivée de l'état → useMemo
- Transformer des données pour le rendu → faire dans le rendu
- Réagir à une action utilisateur → gestionnaire d'événement
Exercices
Exercice 1 : Titre de page dynamique
Créer un composant qui met à jour le titre de la page avec le compteur.
Solution :
- snippet.javascript
function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `Compteur : ${count}`; }, [count]); return ( <div> <p>{count}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> </div> ); }
Exercice 2 : Fetch avec gestion d'erreur
Créer un composant qui charge une liste de posts depuis une API.
Solution :
- snippet.javascript
function PostList() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchPosts = async () => { try { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); if (!response.ok) throw new Error('Erreur réseau'); const data = await response.json(); setPosts(data.slice(0, 10)); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchPosts(); }, []); if (loading) return <p>Chargement...</p>; if (error) return <p>Erreur : {error}</p>; return ( <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }
Points clés à retenir
- useEffect exécute du code après le rendu
- Tableau vide
[]= une seule fois au montage - Avec dépendances = quand ces valeurs changent
- Fonction de retour = nettoyage (cleanup)
- Toujours lister les dépendances utilisées dans l'effet
- Async : définir une fonction async à l'intérieur, pas sur useEffect
← Chapitre précédent | Retour au module | Chapitre suivant : useRef →