Cours JavaScript et React · Module 5: useEffect et hooks personnalisés
Nettoyer les effets
Dans cette leçon10
Nettoyer correctement les effets de bord est un aspect crucial de la construction d'applications React fiables. Tout comme les astronautes doivent entretenir leurs systèmes de survie pour éviter les pannes, nous devons aussi prendre soin de nettoyer nos effets pour éviter les fuites de mémoire, les calculs inutiles et d'autres problèmes.
Pourquoi nettoyer les effets est-il important ?
Lorsqu'un composant est démonté (retiré du DOM) ou lorsque les dépendances d'un effet changent, divers problèmes peuvent survenir si nous ne prenons pas soin d'un nettoyage correct :
- Fuites de mémoire - des ressources inutilisées qui ne sont pas libérées
- Mise à jour de l'état de composants démontés - entraîne des avertissements et des erreurs
- Calculs inutiles - consomment des ressources pour des opérations qui ne sont plus nécessaires
- Mises à jour conflictuelles - lorsque d'anciens et de nouveaux effets s'exécutent simultanément
- Abonnements fantômes - d'anciens abonnements qui continuent de répondre aux événements
Syntaxe de base de la fonction de nettoyage
Dans useEffect, la fonction de nettoyage est retournée depuis la fonction de l'effet :
1useEffect(() => {
2 // Code de l'effet - s'exécute après le rendu
3 console.log('Effet exécuté');
4
5 // Fonction de nettoyage - s'exécute avant le prochain effet ou au démontage
6 return () => {
7 console.log('Effet nettoyé');
8 };
9}, [dependency]); // Tableau de dépendancesQuand la fonction de nettoyage est-elle appelée ?
La fonction de nettoyage est invoquée dans deux situations :
- Avant la ré-exécution de l'effet - lorsque les dépendances changent
- Avant le démontage du composant - lorsque le composant est retiré du DOM
1function SpaceModule({ moduleId }) {
2 useEffect(() => {
3 console.log(`Le module ${moduleId} a été initialisé`);
4
5 // Cette fonction sera appelée :
6 // 1. Avant la prochaine initialisation lorsque moduleId change
7 // 2. Lorsque le composant SpaceModule est démonté
8 return () => {
9 console.log(`Le module ${moduleId} a été arrêté`);
10 };
11 }, [moduleId]);
12
13 return <div className="space-module">Module spatial : {moduleId}</div>;
14}Cas courants nécessitant un nettoyage
1. Intervalles et timeouts
Les intervalles et timeouts non libérés peuvent continuer à s'exécuter en arrière-plan, consommant des ressources et mettant potentiellement à jour l'état de composants qui n'existent plus :
1function Countdown({ seconds, onComplete }) {
2 const [timeLeft, setTimeLeft] = useState(seconds);
3
4 useEffect(() => {
5 // Ne pas démarrer le compte à rebours s'il est déjà terminé
6 if (timeLeft <= 0) {
7 onComplete();
8 return;
9 }
10
11 console.log(`Je commence le compte à rebours à partir de ${timeLeft}`);
12
13 // Mise en place de l'intervalle
14 const intervalId = setInterval(() => {
15 setTimeLeft(prevTime => {
16 if (prevTime <= 1) {
17 onComplete();
18 return 0;
19 }
20 return prevTime - 1;
21 });
22 }, 1000);
23
24 // La fonction de nettoyage arrête l'intervalle
25 return () => {
26 console.log(`Arrêt du compte à rebours à ${timeLeft}`);
27 clearInterval(intervalId);
28 };
29 }, [timeLeft, onComplete]);
30
31 return <div className="countdown">{timeLeft}</div>;
32}2. Abonnements à des sources de données externes
Les abonnements qui ne sont pas annulés peuvent continuer à recevoir des données et provoquer des mises à jour d'état :
1function TelemetryMonitor({ shipId }) {
2 const [data, setData] = useState(null);
3
4 useEffect(() => {
5 let isMounted = true;
6 console.log(`Je commence la surveillance du vaisseau ${shipId}`);
7
8 // Création de l'abonnement à la télémétrie
9 const subscription = telemetryService.subscribe(shipId, telemetryData => {
10 // Mettre à jour l'état uniquement si le composant est toujours monté
11 if (isMounted) {
12 setData(telemetryData);
13 }
14 });
15
16 // La fonction de nettoyage annule l'abonnement
17 return () => {
18 console.log(`Arrêt de la surveillance du vaisseau ${shipId}`);
19 isMounted = false;
20 subscription.unsubscribe();
21 };
22 }, [shipId]);
23
24 if (!data) return <div>En attente des données de télémétrie...</div>;
25
26 return (
27 <div className="telemetry">
28 <h3>Télémétrie du vaisseau : {shipId}</h3>
29 <p>Niveau d'oxygène : {data.oxygen}%</p>
30 <p>Température : {data.temperature}°C</p>
31 <p>Vitesse : {data.velocity} km/h</p>
32 </div>
33 );
34}3. Écouteurs d'événements DOM
Les écouteurs d'événements qui ne sont pas retirés peuvent entraîner des fuites de mémoire :
1function KeyboardShortcuts({ onEscape }) {
2 useEffect(() => {
3 console.log('Installation de l\'écouteur clavier');
4
5 // Fonction gérant l'appui sur une touche
6 const handleKeyDown = (event) => {
7 if (event.key === 'Escape') {
8 onEscape();
9 }
10 };
11
12 // Ajout de l'écouteur au document
13 document.addEventListener('keydown', handleKeyDown);
14
15 // La fonction de nettoyage retire l'écouteur
16 return () => {
17 console.log('Retrait de l\'écouteur clavier');
18 document.removeEventListener('keydown', handleKeyDown);
19 };
20 }, [onEscape]);
21
22 return null; // Ce composant n'affiche rien de visible
23}4. WebSockets et connexions temps réel
Les connexions qui restent ouvertes peuvent consommer des ressources et provoquer des fuites :
1function MissionControlSocket({ missionId, onMessage }) {
2 const [status, setStatus] = useState('disconnected');
3
4 useEffect(() => {
5 let socket = null;
6
7 // Fonction initialisant la connexion
8 const connect = () => {
9 console.log(`Connexion à la mission ${missionId}`);
10 setStatus('connecting');
11
12 // Création du WebSocket
13 socket = new WebSocket(`wss://mission-control.space/mission/${missionId}`);
14
15 socket.onopen = () => {
16 console.log(`Connecté à la mission ${missionId}`);
17 setStatus('connected');
18 };
19
20 socket.onmessage = (event) => {
21 const data = JSON.parse(event.data);
22 onMessage(data);
23 };
24
25 socket.onerror = (error) => {
26 console.error(`Erreur de connexion WebSocket : ${error.message}`);
27 setStatus('error');
28 };
29
30 socket.onclose = () => {
31 console.log(`Déconnecté de la mission ${missionId}`);
32 setStatus('disconnected');
33 };
34 };
35
36 // Initialiser la connexion
37 connect();
38
39 // La fonction de nettoyage ferme la connexion
40 return () => {
41 console.log(`Fermeture de la connexion à la mission ${missionId}`);
42 if (socket) {
43 socket.close();
44 }
45 };
46 }, [missionId, onMessage]);
47
48 return (
49 <div className="socket-status">
50 Statut de la connexion : {status}
51 </div>
52 );
53}5. Annuler des requêtes fetch
Annuler les requêtes fetch en cours empêche les mises à jour d'état après démontage :
1function PlanetInfo({ planetId }) {
2 const [planet, setPlanet] = useState(null);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState(null);
5
6 useEffect(() => {
7 // Création du controller pour annuler la requête
8 const abortController = new AbortController();
9 const signal = abortController.signal;
10
11 async function fetchPlanetData() {
12 try {
13 setLoading(true);
14
15 console.log(`Récupération des données de la planète ${planetId}`);
16 const response = await fetch(`https://api.space.com/planets/${planetId}`, {
17 signal // passer le signal à fetch
18 });
19
20 if (!response.ok) {
21 throw new Error(`Erreur HTTP : ${response.status}`);
22 }
23
24 const data = await response.json();
25 setPlanet(data);
26 setError(null);
27 } catch (err) {
28 // Ignorer les erreurs d'annulation
29 if (err.name !== 'AbortError') {
30 console.error(`Erreur lors de la récupération des données : ${err.message}`);
31 setError(`Échec de la récupération des données de la planète : ${err.message}`);
32 }
33 } finally {
34 setLoading(false);
35 }
36 }
37
38 fetchPlanetData();
39
40 // La fonction de nettoyage annule les requêtes en cours
41 return () => {
42 console.log(`Annulation de la requête pour la planète ${planetId}`);
43 abortController.abort();
44 };
45 }, [planetId]);
46
47 if (loading) return <div>Chargement des données de la planète...</div>;
48 if (error) return <div>Erreur : {error}</div>;
49 if (!planet) return <div>Aucune donnée de planète</div>;
50
51 return (
52 <div className="planet-card">
53 <h2>{planet.name}</h2>
54 <p>Type : {planet.type}</p>
55 <p>Diamètre : {planet.diameter} km</p>
56 <p>Distance au Soleil : {planet.distanceFromSun} millions de km</p>
57 </div>
58 );
59}Modèles avancés de nettoyage des effets
1. L'indicateur isMounted
Bien que l'utilisation d'AbortController soit la méthode préférée pour annuler les requêtes fetch, dans les cas où nous ne pouvons pas facilement annuler une opération asynchrone, nous pouvons utiliser un indicateur isMounted :
1function AsteroidTracker({ asteroidId }) {
2 const [data, setData] = useState(null);
3
4 useEffect(() => {
5 let isMounted = true; // Indicateur signalant si le composant est monté
6
7 async function fetchAsteroidData() {
8 const response = await fetch(`https://api.nasa.gov/neo/rest/v1/neo/${asteroidId}`);
9 const asteroidData = await response.json();
10
11 // Mettre à jour l'état uniquement si le composant est toujours monté
12 if (isMounted) {
13 setData(asteroidData);
14 }
15 }
16
17 fetchAsteroidData();
18
19 // La fonction de nettoyage change l'indicateur
20 return () => {
21 isMounted = false;
22 };
23 }, [asteroidId]);
24
25 // Rendu...
26}2. Synchronisation avec des stockages d'état externes
Lors de la synchronisation de l'état React avec un stockage d'état externe, il est important de restaurer l'état précédent pendant le nettoyage :
1function ThemeSwitcher({ theme }) {
2 useEffect(() => {
3 // Sauvegarder le thème actuel
4 const previousTheme = document.body.dataset.theme;
5
6 // Définir le nouveau thème
7 document.body.dataset.theme = theme;
8 console.log(`Changement de thème vers : ${theme}`);
9
10 // Restaurer le thème précédent pendant le nettoyage
11 return () => {
12 console.log(`Restauration du thème précédent : ${previousTheme || 'default'}`);
13 document.body.dataset.theme = previousTheme || 'default';
14 };
15 }, [theme]);
16
17 return (
18 <div className="theme-info">
19 Thème actuel : {theme}
20 </div>
21 );
22}3. Gérer les ressources partagées
Lorsque plusieurs composants partagent des ressources, il faut utiliser un système de comptage de références :
1// Singleton pour gérer les ressources partagées
2class SharedResourceManager {
3 constructor() {
4 this.resources = new Map();
5 this.refCounts = new Map();
6 }
7
8 // Acquérir une ressource (incrémenter le compteur de références)
9 acquire(resourceId, creator) {
10 if (!this.resources.has(resourceId)) {
11 // Créer la ressource si elle n'existe pas
12 this.resources.set(resourceId, creator());
13 this.refCounts.set(resourceId, 0);
14 }
15
16 // Incrémenter le compteur de références
17 this.refCounts.set(resourceId, this.refCounts.get(resourceId) + 1);
18 return this.resources.get(resourceId);
19 }
20
21 // Libérer une ressource (décrémenter le compteur de références)
22 release(resourceId) {
23 if (!this.resources.has(resourceId)) return;
24
25 // Décrémenter le compteur de références
26 const newCount = this.refCounts.get(resourceId) - 1;
27 this.refCounts.set(resourceId, newCount);
28
29 // Supprimer la ressource lorsqu'il n'y a plus de références
30 if (newCount <= 0) {
31 const resource = this.resources.get(resourceId);
32 if (resource.dispose && typeof resource.dispose === 'function') {
33 resource.dispose();
34 }
35 this.resources.delete(resourceId);
36 this.refCounts.delete(resourceId);
37 }
38 }
39}
40
41// Singleton pour usage dans toute l'application
42export const sharedResources = new SharedResourceManager();
43
44// Hook pour utiliser une ressource partagée
45function useSharedResource(resourceId, createResource) {
46 useEffect(() => {
47 // Acquérir la ressource
48 const resource = sharedResources.acquire(resourceId, createResource);
49 console.log(`Ressource ${resourceId} acquise, nombre d'utilisateurs : ${sharedResources.refCounts.get(resourceId)}`);
50
51 // Libérer la ressource pendant le nettoyage
52 return () => {
53 sharedResources.release(resourceId);
54 console.log(`Ressource ${resourceId} libérée`);
55 };
56 }, [resourceId]);
57
58 return sharedResources.resources.get(resourceId);
59}
60
61// Exemple d'utilisation
62function SpaceshipScanner() {
63 // Cette ressource sera partagée entre toutes les instances du composant
64 const scanner = useSharedResource('advanced-scanner', () => {
65 console.log('Initialisation du scanner (opération coûteuse)');
66 return {
67 scan: (target) => console.log(`Scan de ${target}...`),
68 dispose: () => console.log('Arrêt du scanner')
69 };
70 });
71
72 return (
73 <button onClick={() => scanner.scan('Mars')}>
74 Scanner Mars
75 </button>
76 );
77}Problèmes courants et solutions
1. Débordement de pile lors du nettoyage de minuteries
Problème : Mal arrêter une minuterie peut entraîner un débordement de pile :
1// MAUVAIS : la fonction de nettoyage n'arrête pas correctement le timeout
2useEffect(() => {
3 function tick() {
4 setCount(count + 1);
5 setTimeout(tick, 1000);
6 }
7
8 tick();
9
10 return () => {
11 // Problème : nous n'avons pas de référence vers l'ID du timeout actuel
12 };
13}, [count]); // La dépendance provoque la ré-exécution de l'effetSolution : Conserve l'identifiant du timeout actuel :
1// BON : nous sauvegardons l'ID du timeout pour pouvoir l'annuler
2useEffect(() => {
3 let timeoutId = null;
4
5 function tick() {
6 setCount(prevCount => prevCount + 1);
7 timeoutId = setTimeout(tick, 1000);
8 }
9
10 timeoutId = setTimeout(tick, 1000);
11
12 return () => {
13 clearTimeout(timeoutId);
14 };
15}, []); // Pas de dépendances - nous n'exécutons qu'une seule fois2. Problèmes d'ordre de nettoyage
Problème : Parfois l'ordre de nettoyage compte, surtout lorsque nous avons plusieurs effets imbriqués :
1// Ordre de nettoyage problématique
2useEffect(() => {
3 // Effet externe
4 const resource = initializeResource();
5
6 useEffect(() => {
7 // Effet interne utilisant la ressource
8 resource.use();
9
10 return () => {
11 // Ce nettoyage peut être appelé après la libération de la ressource
12 resource.stopUsing();
13 };
14 }, [someCondition]);
15
16 return () => {
17 // Cette ressource peut déjà être utilisée par l'effet interne
18 resource.dispose();
19 };
20}, []);Solution : Utilise des effets distincts et indépendants :
1// Meilleure approche
2useEffect(() => {
3 // Effet initialisant la ressource
4 const resource = initializeResource();
5
6 return () => {
7 // Nettoyage de la ressource
8 resource.dispose();
9 };
10}, []);
11
12useEffect(() => {
13 // Effet utilisant la ressource (utilise useRef ou un contexte)
14 if (resourceRef.current) {
15 resourceRef.current.use();
16
17 return () => {
18 if (resourceRef.current) {
19 resourceRef.current.stopUsing();
20 }
21 };
22 }
23}, [someCondition]);3. Incohérence de l'état pendant le nettoyage
Problème : Pendant le nettoyage, nous pouvons accéder à un état obsolète :
1function SpaceshipControl({ shipId }) {
2 const [power, setPower] = useState(100);
3
4 useEffect(() => {
5 // Initialisation du contrôle du vaisseau
6 const ship = spaceshipAPI.connect(shipId);
7 ship.setPower(power);
8
9 return () => {
10 // Problème : power peut être obsolète pendant le nettoyage
11 console.log(`Déconnexion du vaisseau à la puissance : ${power}`);
12 ship.disconnect();
13 };
14 }, [shipId]); // Pas de dépendance sur power
15
16 return (
17 <div>
18 <p>Puissance du moteur : {power}%</p>
19 <button onClick={() => setPower(power - 10)}>Réduire la puissance</button>
20 </div>
21 );
22}Solution : Utilise des références pour stocker les valeurs d'état actuelles :
1function SpaceshipControl({ shipId }) {
2 const [power, setPower] = useState(100);
3 const powerRef = useRef(power);
4
5 // Mise à jour de la référence à chaque changement d'état
6 useEffect(() => {
7 powerRef.current = power;
8 }, [power]);
9
10 useEffect(() => {
11 // Initialisation du contrôle du vaisseau
12 const ship = spaceshipAPI.connect(shipId);
13 ship.setPower(power);
14
15 return () => {
16 // Nous utilisons la valeur actuelle depuis la ref
17 console.log(`Déconnexion du vaisseau à la puissance : ${powerRef.current}%`);
18 ship.disconnect();
19 };
20 }, [shipId, power]);
21
22 return (
23 <div>
24 <p>Puissance du moteur : {power}%</p>
25 <button onClick={() => setPower(power - 10)}>Réduire la puissance</button>
26 </div>
27 );
28}Tester le nettoyage des effets
Pour s'assurer que les effets sont correctement nettoyés, il vaut la peine d'écrire des tests qui vérifient ce comportement :
1// Exemple de test pour un composant avec nettoyage d'effet
2import { render, act, screen } from '@testing-library/react';
3import { useState } from 'react';
4
5// Composant simple avec minuterie pour les tests
6function TimerComponent() {
7 const [count, setCount] = useState(0);
8
9 useEffect(() => {
10 const interval = setInterval(() => {
11 setCount(c => c + 1);
12 }, 100);
13
14 return () => clearInterval(interval);
15 }, []);
16
17 return <div data-testid="count">{count}</div>;
18}
19
20// Test du nettoyage d'effet
21test('le nettoyage de l\'effet arrête l\'intervalle', async () => {
22 jest.useFakeTimers();
23
24 const { unmount } = render(<TimerComponent />);
25
26 // Laisser passer quelques ticks de la minuterie
27 act(() => {
28 jest.advanceTimersByTime(250);
29 });
30
31 const countBefore = Number(screen.getByTestId('count').textContent);
32
33 // Démonter le composant (devrait nettoyer l'effet et arrêter la minuterie)
34 unmount();
35
36 // Avancer l'horloge une fois de plus
37 act(() => {
38 jest.advanceTimersByTime(250);
39 });
40
41 // Comme le composant est démonté et la minuterie arrêtée,
42 // nous pouvons seulement vérifier que le compteur n'a pas trop augmenté avant le démontage
43 expect(countBefore).toBeGreaterThanOrEqual(2);
44 expect(countBefore).toBeLessThanOrEqual(3);
45
46 jest.useRealTimers();
47});Bonnes pratiques pour nettoyer les effets
Nettoie toujours les effets qui créent des abonnements - minuteries, écouteurs d'événements, connexions WebSocket, etc.
Utilise AbortController pour annuler les requêtes fetch - c'est le mécanisme standard pour annuler les opérations asynchrones.
Maintiens un indicateur de montage - pour les opérations asynchrones qui ne peuvent pas être annulées, utilise un indicateur
isMountedet vérifie-le avant de mettre à jour l'état.Stocke les références vers les ressources - les variables locales d'un effet cessent d'exister après le nettoyage, alors utilise
useRefpour stocker les références aux ressources.Évite les effets trop complexes - un effet qui fait trop de choses sera plus difficile à nettoyer correctement. Envisage de le diviser en effets plus petits et plus ciblés.
Évite les fonctions de nettoyage asynchrones - les fonctions de nettoyage doivent être synchrones pour que React puisse garantir leur exécution dans le bon ordre.
Teste le nettoyage des effets - il est particulièrement important de tester le démontage de composants qui créent des abonnements externes.
Le nettoyage des effets et les performances
Un nettoyage correct des effets ne prévient pas seulement les fuites de mémoire, il peut aussi avoir un impact significatif sur les performances de l'application :
Moins d'utilisation de la mémoire - en libérant les ressources inutilisées
Moins de calculs inutiles - en annulant les opérations qui ne sont plus nécessaires
Moins de re-rendus - en évitant les mises à jour d'état inutiles
Rechargements de page plus rapides - en s'assurant que toutes les ressources sont correctement libérées
Résumé
Nettoyer les effets est un aspect souvent négligé mais crucial du travail avec React. Tout comme les astronautes doivent prendre soin de leurs systèmes de survie, les développeurs doivent prendre soin de nettoyer leurs effets de bord.
Des fonctions de nettoyage bien écrites :
- Préviennent les fuites de mémoire
- Évitent de mettre à jour l'état de composants démontés
- Libèrent les ressources système
- Assurent la cohérence de l'application
Souviens-toi que chaque effet qui crée une connexion, un abonnement ou réserve des ressources devrait aussi les libérer pendant le nettoyage.
Code de cette leçon : App.jsx
1import React, { useState, useEffect } from 'react';
2
3// Démonstration du cleanup avec les timers
4function TimerWithCleanup() {
5 const [count, setCount] = useState(0);
6 const [isRunning, setIsRunning] = useState(false);
7
8 useEffect(() => {
9 if (!isRunning) return;
10
11 console.log('⏰ Timer started');
12 const interval = setInterval(() => {
13 setCount(c => c + 1);
14 }, 1000);
15
16 // CLEANUP - très important !
17 return () => {
18 console.log('Timer stopped (cleanup)');
19 clearInterval(interval);
20 };
21 }, [isRunning]);
22
23 return (
24 <div className="demo-card">
25 <h2>⏱Minuterie avec nettoyage</h2>
26 <div className="counter">{count}s</div>
27 <div className="controls">
28 <button onClick={() => setIsRunning(true)} disabled={isRunning}>
29 ▶Start
30 </button>
31 <button onClick={() => setIsRunning(false)} disabled={!isRunning}>
32 ⏹Stop
33 </button>
34 <button onClick={() => { setCount(0); setIsRunning(false); }}>
35 Reset
36 </button>
37 </div>
38 <p className="hint">Ouvre la console pour voir les journaux de nettoyage</p>
39 </div>
40 );
41}
42
43// Démonstration du cleanup avec les event listeners
44function MouseTrackerWithCleanup() {
45 const [position, setPosition] = useState({ x: 0, y: 0 });
46 const [tracking, setTracking] = useState(false);
47
48 useEffect(() => {
49 if (!tracking) return;
50
51 const handleMouseMove = (e) => {
52 setPosition({ x: e.clientX, y: e.clientY });
53 };
54
55 console.log('Mouse tracking enabled');
56 window.addEventListener('mousemove', handleMouseMove);
57
58 // CLEANUP - suppression de l'event listener
59 return () => {
60 console.log('Mouse tracking disabled (cleanup)');
61 window.removeEventListener('mousemove', handleMouseMove);
62 };
63 }, [tracking]);
64
65 return (
66 <div className="demo-card">
67 <h2>Suivi de la souris</h2>
68 <div className="position">
69 X: {position.x} | Y: {position.y}
70 </div>
71 <button onClick={() => setTracking(!tracking)}>
72 {tracking ? 'Désactiver le suivi' : 'Activer le suivi'}
73 </button>
74 <p className="hint">
75 {tracking ? 'Bouge la souris !' : 'Clique pour activer'}
76 </p>
77 </div>
78 );
79}
80
81// Démonstration du cleanup avec les abonnements
82function SubscriptionWithCleanup() {
83 const [messages, setMessages] = useState([]);
84 const [subscribed, setSubscribed] = useState(false);
85
86 useEffect(() => {
87 if (!subscribed) return;
88
89 console.log('Subscribed to space channel');
90
91 // Simulation d'abonnement (ex. WebSocket)
92 const interval = setInterval(() => {
93 const newMessage = {
94 id: Date.now(),
95 text: `Transmisja #${Math.floor(Math.random() * 1000)}`,
96 time: new Date().toLocaleTimeString()
97 };
98 setMessages(prev => [...prev.slice(-4), newMessage]);
99 }, 2000);
100
101 // CLEANUP - annulation de l'abonnement
102 return () => {
103 console.log('Unsubscribed from space channel (cleanup)');
104 clearInterval(interval);
105 };
106 }, [subscribed]);
107
108 return (
109 <div className="demo-card">
110 <h2>Abonnement</h2>
111 <div className="messages">
112 {messages.length === 0 ? (
113 <p className="empty">Aucun message</p>
114 ) : (
115 messages.map(msg => (
116 <div key={msg.id} className="message">
117 [{msg.time}] {msg.text}
118 </div>
119 ))
120 )}
121 </div>
122 <button onClick={() => {
123 setSubscribed(!subscribed);
124 if (subscribed) setMessages([]);
125 }}>
126 {subscribed ? "Se désabonner" : "S'abonner"}
127 </button>
128 </div>
129 );
130}
131
132// Composant montrant un problème de fuite de mémoire
133function MemoryLeakDemo() {
134 const [showChild, setShowChild] = useState(true);
135 const [logs, setLogs] = useState([]);
136
137 const addLog = (msg) => setLogs(prev => [...prev.slice(-5), msg]);
138
139 return (
140 <div className="demo-card wide">
141 <h2>Démo des fuites de mémoire</h2>
142 <button onClick={() => {
143 setShowChild(!showChild);
144 addLog(showChild ? 'Composant démonté' : 'Composant monté');
145 }}>
146 {showChild ? 'Démonter le composant' : 'Monter le composant'}
147 </button>
148
149 <div className="child-container">
150 {showChild && <ChildWithCleanup onLog={addLog} />}
151 </div>
152
153 <div className="logs">
154 <h3>Journaux :</h3>
155 {logs.map((log, i) => <div key={i}>{log}</div>)}
156 </div>
157 </div>
158 );
159}
160
161function ChildWithCleanup({ onLog }) {
162 useEffect(() => {
163 onLog('Effect exécuté');
164
165 const timer = setInterval(() => {
166 onLog('⏰ Timer tick');
167 }, 2000);
168
169 return () => {
170 onLog('Cleanup exécuté - timer nettoyé');
171 clearInterval(timer);
172 };
173 }, []);
174
175 return <div className="child">Composant enfant (avec effet)</div>;
176}
177
178function App() {
179 return (
180 <div className="app">
181 <h1>Nettoyage des effets (Cleanup)</h1>
182 <p className="subtitle">Gestion des ressources dans useEffect</p>
183 <div className="demos">
184 <TimerWithCleanup />
185 <MouseTrackerWithCleanup />
186 <SubscriptionWithCleanup />
187 <MemoryLeakDemo />
188 </div>
189 </div>
190 );
191}
192
193export default App;Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle est la meilleure façon d'organiser plusieurs effets dans un composant ?
2. Comment gérer correctement les erreurs dans les opérations asynchrones au sein de useEffect ?
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne les éléments de la syntaxe de nettoyage dans useEffect, de gauche à droite :
- Éditeur de code
Synchronisation des réglages avec LocalStorage
- Clique dans l'ordre
Ordonne la syntaxe de useEffect avec une fonction de nettoyage :
- Tri vertical
Ordonne les modèles avancés de useEffect, du plus basique au plus complexe :
- Éditeur de code
Intersection Observer pour le chargement différé