Cours JavaScript et React · Module 5: useEffect et hooks personnalisés

Nettoyer les effets

14 min de lecture
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 :

  1. Fuites de mémoire - des ressources inutilisées qui ne sont pas libérées
  2. Mise à jour de l'état de composants démontés - entraîne des avertissements et des erreurs
  3. Calculs inutiles - consomment des ressources pour des opérations qui ne sont plus nécessaires
  4. Mises à jour conflictuelles - lorsque d'anciens et de nouveaux effets s'exécutent simultanément
  5. 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épendances

Quand la fonction de nettoyage est-elle appelée ?

La fonction de nettoyage est invoquée dans deux situations :

  1. Avant la ré-exécution de l'effet - lorsque les dépendances changent
  2. 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'effet

Solution : 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 fois

2. 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

  1. Nettoie toujours les effets qui créent des abonnements - minuteries, écouteurs d'événements, connexions WebSocket, etc.

  2. Utilise AbortController pour annuler les requêtes fetch - c'est le mécanisme standard pour annuler les opérations asynchrones.

  3. Maintiens un indicateur de montage - pour les opérations asynchrones qui ne peuvent pas être annulées, utilise un indicateur isMounted et vérifie-le avant de mettre à jour l'état.

  4. Stocke les références vers les ressources - les variables locales d'un effet cessent d'exister après le nettoyage, alors utilise useRef pour stocker les références aux ressources.

  5. É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.

  6. É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.

  7. 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 :

  1. Moins d'utilisation de la mémoire - en libérant les ressources inutilisées

  2. Moins de calculs inutiles - en annulant les opérations qui ne sont plus nécessaires

  3. Moins de re-rendus - en évitant les mises à jour d'état inutiles

  4. 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. 1. Quelle est la meilleure façon d'organiser plusieurs effets dans un composant ?

  2. 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é

Articles utiles