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

Synchronisation avec des systèmes externes

12 min de lecture
Dans cette leçon7

Dans le module précédent, nous avons appris les bases des effets de bord avec le hook useEffect. Maintenant, nous allons approfondir l'un des cas d'usage les plus importants des effets : synchroniser les composants React avec des systèmes et des bibliothèques externes.

Pourquoi la synchronisation est-elle importante ?

Les composants React existent dans leur propre environnement réactif, mais ils doivent souvent interagir avec des systèmes indépendants qui ignorent le cycle de vie ou le modèle de rendu de React. Tu peux voir cela comme un pont entre le vaisseau spatial React et d'autres systèmes - la communication doit être bidirectionnelle et fiable.

Voici des exemples de tels systèmes externes :

  • Bibliothèques de cartes (ex. Google Maps, Leaflet)
  • Bibliothèques de graphiques (ex. D3.js, Chart.js)
  • Lecteurs multimédias (ex. HTML5 Video, Web Audio API)
  • SDK WebSocket et communication temps réel
  • Systèmes de gestion de formulaires (ex. Formik, React Hook Form)
  • Bibliothèques d'animation (ex. GSAP, Anime.js)
  • Interfaces du navigateur (géolocalisation, Web Bluetooth)

Modèle de synchronisation de base

Le modèle de base pour la synchronisation avec un système externe se présente comme suit :

  1. Initialise la bibliothèque/le système externe après le premier rendu
  2. Mets à jour le système externe lorsque les props ou l'état pertinents changent
  3. Nettoie (arrête, déconnecte) lorsque le composant est démonté
1function ExternalSystemComponent({ config }) {
2  useEffect(() => {
3    // 1. Initialisation
4    const externalInstance = new ExternalSystem(config);
5    externalInstance.initialize();
6
7    // 2. Mise à jour (optionnelle)
8    externalInstance.update(config);
9
10    // 3. Nettoyage
11    return () => {
12      externalInstance.cleanup();
13    };
14  }, [config]); // Re-synchroniser lorsque config change
15
16  return <div className="external-system-container" />;
17}

Exemples pratiques de synchronisation

1. Intégration avec une bibliothèque de cartes (Leaflet)

Leaflet est une bibliothèque populaire de cartes interactives qui fonctionne indépendamment de React. Voici un exemple d'intégration :

1import React, { useEffect, useRef } from 'react';
2import L from 'leaflet';
3import 'leaflet/dist/leaflet.css';
4
5function MarsExplorationMap({ missionCoordinates, roverPosition, zoom = 5 }) {
6  // Référence vers le conteneur de la carte
7  const mapContainerRef = useRef(null);
8  // Référence vers l'instance de la carte (pour éviter de la recréer)
9  const mapInstanceRef = useRef(null);
10  // Référence vers le marqueur du rover (pour mettre à jour uniquement sa position)
11  const roverMarkerRef = useRef(null);
12
13  // Initialisation de la carte
14  useEffect(() => {
15    if (!mapInstanceRef.current && mapContainerRef.current) {
16      // Créer l'instance de la carte une seule fois
17      const mapInstance = L.map(mapContainerRef.current).setView(
18        [missionCoordinates.lat, missionCoordinates.lng],
19        zoom
20      );
21
22      // Ajouter la couche de tuiles
23      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
24        attribution: '© OpenStreetMap contributors'
25      }).addTo(mapInstance);
26
27      // Sauvegarder l'instance de la carte dans la référence
28      mapInstanceRef.current = mapInstance;
29
30      // Ajouter le marqueur du rover
31      roverMarkerRef.current = L.marker([roverPosition.lat, roverPosition.lng])
32        .addTo(mapInstance)
33        .bindPopup('Mars Rover');
34    }
35
36    // Fonction de nettoyage
37    return () => {
38      if (mapInstanceRef.current) {
39        // Retirer la carte lorsque le composant est démonté
40        mapInstanceRef.current.remove();
41        mapInstanceRef.current = null;
42        roverMarkerRef.current = null;
43      }
44    };
45  }, []); // Tableau de dépendances vide - initialiser une seule fois
46
47  // Mise à jour de la position du rover
48  useEffect(() => {
49    if (roverMarkerRef.current && roverPosition) {
50      // Mettre à jour la position du marqueur existant
51      roverMarkerRef.current.setLatLng([roverPosition.lat, roverPosition.lng]);
52    }
53  }, [roverPosition]); // Mettre à jour uniquement lorsque la position du rover change
54
55  // Mise à jour du niveau de zoom
56  useEffect(() => {
57    if (mapInstanceRef.current) {
58      mapInstanceRef.current.setZoom(zoom);
59    }
60  }, [zoom]); // Mettre à jour uniquement lorsque le zoom change
61
62  return <div ref={mapContainerRef} style={{ height: '500px', width: '100%' }} />;
63}

Dans l'exemple ci-dessus :

  1. Nous utilisons des références (useRef) pour stocker les éléments DOM et les instances de la bibliothèque externe
  2. Nous initialisons la carte une seule fois et sauvegardons son instance
  3. Nous utilisons des effets distincts pour gérer différents aspects de la synchronisation
  4. Nous nettoyons les ressources lorsque le composant est démonté

2. Intégration avec une bibliothèque de graphiques (Chart.js)

Chart.js est une bibliothèque populaire pour créer des graphiques interactifs :

1import React, { useEffect, useRef } from 'react';
2import Chart from 'chart.js/auto';
3
4function MissionResourcesChart({ resources, title }) {
5  const chartRef = useRef(null);
6  const chartInstanceRef = useRef(null);
7
8  useEffect(() => {
9    // S'assurer que nous avons accès à l'élément canvas
10    if (!chartRef.current) return;
11
12    // Obtenir le contexte du canvas
13    const ctx = chartRef.current.getContext('2d');
14
15    // Détruire l'instance de graphique précédente si elle existe
16    if (chartInstanceRef.current) {
17      chartInstanceRef.current.destroy();
18    }
19
20    // Créer une nouvelle instance de graphique
21    chartInstanceRef.current = new Chart(ctx, {
22      type: 'bar',
23      data: {
24        labels: resources.map(resource => resource.name),
25        datasets: [{
26          label: title || 'Ressources de la mission',
27          data: resources.map(resource => resource.level),
28          backgroundColor: resources.map(resource => {
29            // Colorer les barres selon le niveau de ressource
30            if (resource.level < 20) return 'rgba(255, 0, 0, 0.7)'; // Critique
31            if (resource.level < 50) return 'rgba(255, 165, 0, 0.7)'; // Avertissement
32            return 'rgba(0, 128, 0, 0.7)'; // OK
33          }),
34          borderColor: 'rgba(0, 0, 0, 0.1)',
35          borderWidth: 1
36        }]
37      },
38      options: {
39        responsive: true,
40        scales: {
41          y: {
42            beginAtZero: true,
43            max: 100
44          }
45        }
46      }
47    });
48
49    // Nettoyage
50    return () => {
51      if (chartInstanceRef.current) {
52        chartInstanceRef.current.destroy();
53        chartInstanceRef.current = null;
54      }
55    };
56  }, [resources, title]); // Recréer le graphique lorsque les données ou le titre changent
57
58  return <canvas ref={chartRef} />;
59}

3. Gérer les Web Sockets

L'intégration des Web Sockets permet une communication en temps réel :

1import React, { useEffect, useState } from 'react';
2
3function MissionControlSocket({ missionId, onMessage }) {
4  const [status, setStatus] = useState('disconnected');
5  const [error, setError] = useState(null);
6
7  useEffect(() => {
8    // Initialisation du socket
9    const socket = new WebSocket(`wss://mission-control.example.com/ws/${missionId}`);
10
11    // Configuration des événements
12    socket.onopen = () => {
13      setStatus('connected');
14      console.log(`Connecté à la mission ${missionId}`);
15    };
16
17    socket.onmessage = (event) => {
18      const data = JSON.parse(event.data);
19      onMessage(data);
20    };
21
22    socket.onerror = (err) => {
23      setStatus('error');
24      setError('Erreur de connexion avec le centre de contrôle de mission');
25      console.error('Erreur WebSocket :', err);
26    };
27
28    socket.onclose = () => {
29      setStatus('disconnected');
30      console.log('Connexion fermée');
31    };
32
33    // Fonction de nettoyage
34    return () => {
35      // Fermer la connexion au démontage
36      if (socket.readyState === WebSocket.OPEN) {
37        socket.close();
38      }
39    };
40  }, [missionId, onMessage]); // Se reconnecter lorsque missionId change
41
42  return (
43    <div className="mission-socket">
44      <div className={`connection-status ${status}`}>
45        Statut de la connexion : {status}
46      </div>
47      {error && <div className="connection-error">{error}</div>}
48    </div>
49  );
50}

Stratégies pour une synchronisation efficace

1. Minimiser les ré-initialisations

Évite de ré-initialiser inutilement les systèmes externes lorsque seule une partie de la configuration change. À la place :

  1. Utilise des effets distincts pour différents aspects de la synchronisation
  2. Utilise des références pour stocker les instances des bibliothèques externes
  3. Mets à jour les instances existantes au lieu d'en créer de nouvelles
1// MAUVAIS - un seul effet ré-initialise tout
2useEffect(() => {
3  const chart = new ChartLibrary(config);
4  return () => chart.destroy();
5}, [config]); // Chaque changement de config provoque une ré-initialisation
6
7// BON - séparer l'initialisation et la mise à jour
8// Référence pour stocker l'instance
9const chartRef = useRef(null);
10
11// Initialiser une seule fois
12useEffect(() => {
13  chartRef.current = new ChartLibrary(containerRef.current);
14  return () => chartRef.current.destroy();
15}, []);
16
17// Mettre à jour la configuration sans ré-initialisation
18useEffect(() => {
19  if (chartRef.current) {
20    chartRef.current.updateConfig(config);
21  }
22}, [config]);

2. Gérer les abonnements

Lorsque tu travailles avec des sources de données externes qui utilisent des abonnements :

  1. Annule toujours les abonnements dans la fonction de nettoyage
  2. Mets à jour les abonnements lorsque les paramètres changent
1function TelemetryMonitor({ satelliteId, dataType }) {
2  const [data, setData] = useState(null);
3
4  useEffect(() => {
5    // Créer l'abonnement
6    const subscription = telemetryAPI.subscribe(
7      satelliteId,
8      dataType,
9      (newData) => setData(newData)
10    );
11
12    // Nettoyage
13    return () => subscription.unsubscribe();
14  }, [satelliteId, dataType]); // Se réabonner lorsque l'ID ou le type de données change
15
16  return <div>{/* Affichage des données */}</div>;
17}

3. Initialisation paresseuse des bibliothèques lourdes

Pour les bibliothèques externes lourdes, tu peux retarder leur initialisation jusqu'à ce qu'elles soient réellement nécessaires :

1function HeavyChart({ data, shouldRender }) {
2  const [chartInstance, setChartInstance] = useState(null);
3  const containerRef = useRef(null);
4
5  // Initialisation paresseuse
6  useEffect(() => {
7    if (!shouldRender) {
8      // Ne pas initialiser si nous ne devons pas encore afficher
9      return;
10    }
11
12    // Import dynamique de la bibliothèque lourde
13    import('heavy-chart-library').then(ChartLib => {
14      const instance = new ChartLib.Chart(containerRef.current, data);
15      setChartInstance(instance);
16
17      return () => {
18        instance.destroy();
19      };
20    });
21  }, [shouldRender]); // S'exécuter uniquement lorsque shouldRender passe à true
22
23  // Mise à jour des données
24  useEffect(() => {
25    if (chartInstance && data) {
26      chartInstance.updateData(data);
27    }
28  }, [chartInstance, data]);
29
30  return <div ref={containerRef} />;
31}

Créer des hooks personnalisés pour la synchronisation

Pour améliorer la réutilisation du code et encapsuler la logique de synchronisation, il vaut la peine de créer des hooks personnalisés.

Exemple : Hook personnalisé pour l'intégration d'une carte

1function useLeafletMap(containerRef, initialConfig) {
2  const mapRef = useRef(null);
3  const [isReady, setIsReady] = useState(false);
4
5  // Initialisation
6  useEffect(() => {
7    if (!containerRef.current || mapRef.current) return;
8
9    const map = L.map(containerRef.current, initialConfig);
10    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
11
12    mapRef.current = map;
13    setIsReady(true);
14
15    return () => {
16      map.remove();
17      mapRef.current = null;
18      setIsReady(false);
19    };
20  }, [containerRef, initialConfig]);
21
22  // Méthodes pour manipuler la carte
23  const setView = useCallback((center, zoom) => {
24    if (mapRef.current) {
25      mapRef.current.setView(center, zoom);
26    }
27  }, []);
28
29  const addMarker = useCallback((position, options = {}) => {
30    if (!mapRef.current) return null;
31
32    const marker = L.marker(position, options).addTo(mapRef.current);
33    return marker;
34  }, []);
35
36  return {
37    map: mapRef.current,
38    isReady,
39    setView,
40    addMarker
41  };
42}
43
44// Utilisation du hook personnalisé
45function MarsMap() {
46  const containerRef = useRef(null);
47  const { map, isReady, setView, addMarker } = useLeafletMap(
48    containerRef,
49    { center: [24.7136, 46.6753], zoom: 5 }
50  );
51
52  // Ajouter un marqueur après le chargement de la carte
53  useEffect(() => {
54    if (isReady) {
55      const marker = addMarker([24.7136, 46.6753], { title: 'Base Alpha' });
56      marker.bindPopup('Base Alpha - Colonie principale sur Mars').openPopup();
57    }
58  }, [isReady, addMarker]);
59
60  return (
61    <div>
62      <h2>Carte de la colonie martienne</h2>
63      <div ref={containerRef} style={{ height: '500px' }} />
64      {!isReady && <div>Chargement de la carte...</div>}
65    </div>
66  );
67}

Exemple : Hook personnalisé pour le stockage local

1function useLocalStorage(key, initialValue) {
2  // Fonction d'initialisation de l'état - s'exécute une seule fois
3  const [storedValue, setStoredValue] = useState(() => {
4    try {
5      // Vérifier si localStorage est disponible
6      if (typeof window === 'undefined') {
7        return initialValue;
8      }
9
10      // Essayer d'obtenir la valeur depuis localStorage
11      const item = window.localStorage.getItem(key);
12      return item ? JSON.parse(item) : initialValue;
13    } catch (error) {
14      console.error('Erreur de lecture depuis localStorage :', error);
15      return initialValue;
16    }
17  });
18
19  // Fonction pour mettre à jour la valeur dans localStorage
20  const setValue = useCallback((value) => {
21    try {
22      // Prendre en charge une fonction comme valeur (comme dans setState)
23      const valueToStore = value instanceof Function ? value(storedValue) : value;
24
25      // Mettre à jour l'état React
26      setStoredValue(valueToStore);
27
28      // Mettre à jour localStorage
29      if (typeof window !== 'undefined') {
30        window.localStorage.setItem(key, JSON.stringify(valueToStore));
31      }
32    } catch (error) {
33      console.error('Erreur d\'écriture dans localStorage :', error);
34    }
35  }, [key, storedValue]);
36
37  // Synchroniser avec localStorage lorsque la clé change
38  useEffect(() => {
39    // Obtenir la valeur actuelle pour la nouvelle clé
40    try {
41      if (typeof window === 'undefined') return;
42
43      const item = window.localStorage.getItem(key);
44      setStoredValue(item ? JSON.parse(item) : initialValue);
45    } catch (error) {
46      console.error('Erreur de synchronisation avec localStorage :', error);
47    }
48  }, [key, initialValue]);
49
50  return [storedValue, setValue];
51}
52
53// Utilisation du hook personnalisé
54function AstronautSettings() {
55  const [settings, setSettings] = useLocalStorage('astronaut-settings', {
56    oxygenAlert: 20,
57    darkMode: false,
58    notifications: true
59  });
60
61  const toggleDarkMode = () => {
62    setSettings(prev => ({
63      ...prev,
64      darkMode: !prev.darkMode
65    }));
66  };
67
68  return (
69    <div className={settings.darkMode ? 'dark-theme' : 'light-theme'}>
70      <h2>Réglages de l'astronaute</h2>
71      <button onClick={toggleDarkMode}>
72        Passer en mode {settings.darkMode ? 'clair' : 'sombre'}
73      </button>
74      <div>
75        Alerte niveau d'oxygène : {settings.oxygenAlert}%
76        <input
77          type="range"
78          min="5"
79          max="50"
80          value={settings.oxygenAlert}
81          onChange={e => setSettings({
82            ...settings,
83            oxygenAlert: Number(e.target.value)
84          })}
85        />
86      </div>
87      <label>
88        <input
89          type="checkbox"
90          checked={settings.notifications}
91          onChange={() => setSettings(prev => ({
92            ...prev,
93            notifications: !prev.notifications
94          }))}
95        />
96        Activer les notifications
97      </label>
98    </div>
99  );
100}

Gérer les effets dans les tests

Tester des composants qui se synchronisent avec des systèmes externes nécessite une approche particulière :

  1. Simuler (mock) les bibliothèques externes
  2. Tester les fonctions de nettoyage
  3. Vérifier que la synchronisation fonctionne correctement

Exemple de test pour un composant WebSocket :

1import { render, act, screen, waitFor } from '@testing-library/react';
2import MissionControlSocket from './MissionControlSocket';
3
4// Mock pour WebSocket
5class MockWebSocket {
6  constructor(url) {
7    this.url = url;
8    this.readyState = WebSocket.CONNECTING;
9
10    // Se « connecter » automatiquement après la création
11    setTimeout(() => {
12      this.readyState = WebSocket.OPEN;
13      this.onopen && this.onopen();
14    }, 0);
15  }
16
17  // Méthodes pour simuler des événements externes
18  simulateMessage(data) {
19    this.onmessage && this.onmessage({ data: JSON.stringify(data) });
20  }
21
22  simulateError() {
23    this.onerror && this.onerror(new Error('Erreur de test'));
24  }
25
26  simulateClose() {
27    this.readyState = WebSocket.CLOSED;
28    this.onclose && this.onclose();
29  }
30
31  close() {
32    this.simulateClose();
33  }
34}
35
36// Remplacer la classe globale WebSocket
37global.WebSocket = MockWebSocket;
38
39describe('MissionControlSocket', () => {
40  test('devrait se connecter au WebSocket et gérer les messages', async () => {
41    const mockOnMessage = jest.fn();
42
43    render(
44      <MissionControlSocket
45        missionId="apollo11"
46        onMessage={mockOnMessage}
47      />
48    );
49
50    // Attendre la connexion
51    await waitFor(() => {
52      expect(screen.getByText(/statut de la connexion : connected/i)).toBeInTheDocument();
53    });
54
55    // Simuler la réception d'un message
56    const testData = { type: 'telemetry', oxygen: 95, fuel: 76 };
57    act(() => {
58      // Accéder à l'instance WebSocket via monkeypatching
59      const socket = global.WebSocket.instances[0];
60      socket.simulateMessage(testData);
61    });
62
63    // Vérifier si le callback a été appelé avec les bonnes données
64    expect(mockOnMessage).toHaveBeenCalledWith(testData);
65
66    // Simuler une erreur
67    act(() => {
68      const socket = global.WebSocket.instances[0];
69      socket.simulateError();
70    });
71
72    // Vérifier si le statut d'erreur est visible
73    expect(screen.getByText(/statut de la connexion : error/i)).toBeInTheDocument();
74    expect(screen.getByText(/erreur de connexion/i)).toBeInTheDocument();
75  });
76
77  test('devrait nettoyer la connexion WebSocket au démontage', () => {
78    const { unmount } = render(
79      <MissionControlSocket missionId="apollo11" onMessage={() => {}} />
80    );
81
82    // Obtenir l'instance WebSocket
83    const socket = global.WebSocket.instances[0];
84    const closeSpy = jest.spyOn(socket, 'close');
85
86    // Démonter le composant
87    unmount();
88
89    // Vérifier si le socket a été fermé
90    expect(closeSpy).toHaveBeenCalled();
91  });
92});

Résumé

La synchronisation avec des systèmes externes est l'une des fonctions les plus importantes du hook useEffect. Elle permet d'utiliser des bibliothèques et des API existantes au sein de l'écosystème React tout en conservant le modèle de mise à jour réactif.

Principes clés :

  1. Initialise les systèmes externes après le premier rendu
  2. Mets à jour ces systèmes lorsque les données pertinentes changent
  3. Nettoie les ressources lorsque le composant est démonté
  4. Sépare les effets selon leur responsabilité
  5. Encapsule la logique de synchronisation dans des hooks dédiés
  6. Minimise les ré-initialisations inutiles
  7. Teste la justesse de la synchronisation et du nettoyage

Souviens-toi que le but principal de la synchronisation est de maintenir la cohérence entre le monde réactif de React et les systèmes externes qui ne connaissent rien du cycle de vie des composants. Avec une synchronisation correcte, ton application fonctionnera de façon fluide et efficace, reliant différentes technologies en un tout cohérent.

Code de cette leçon : App.jsx
1import React, { useState, useEffect, useRef } from 'react';
2
3// Synchronisation avec une API externe - système de navigation spatiale
4function SpaceNavigationSystem() {
5  const [position, setPosition] = useState({ x: 0, y: 0 });
6  const [isTracking, setIsTracking] = useState(true);
7  const [lastSync, setLastSync] = useState(null);
8
9  // Synchronisation avec le système de navigation "externe"
10  useEffect(() => {
11    if (!isTracking) return;
12
13    // Simulation du système de navigation externe
14    const interval = setInterval(() => {
15      setPosition(prev => ({
16        x: prev.x + (Math.random() - 0.5) * 10,
17        y: prev.y + (Math.random() - 0.5) * 10
18      }));
19      setLastSync(new Date().toLocaleTimeString());
20    }, 1000);
21
22    // Cleanup - déconnexion du système
23    return () => {
24      console.log('Disconnecting from navigation system...');
25      clearInterval(interval);
26    };
27  }, [isTracking]);
28
29  return (
30    <div className="nav-system">
31      <h2>Système de navigation cosmique</h2>
32      <div className="status">
33        Statut : {isTracking ? 'Actif' : 'Inactif'}
34      </div>
35      <div className="coordinates">
36        <p>Coordonnée X : {position.x.toFixed(2)}</p>
37        <p>Coordonnée Y : {position.y.toFixed(2)}</p>
38      </div>
39      {lastSync && <p className="sync-time">Dernière synchronisation : {lastSync}</p>}
40      <button onClick={() => setIsTracking(!isTracking)}>
41        {isTracking ? 'Arrêter le suivi' : 'Reprendre le suivi'}
42      </button>
43    </div>
44  );
45}
46
47// Synchronisation avec le DOM - bibliothèque de graphiques externe
48function ExternalChartSync() {
49  const chartRef = useRef(null);
50  const [data, setData] = useState([30, 50, 40, 60, 45, 70, 55]);
51
52  // Synchronisation avec la "bibliothèque de graphiques externe"
53  useEffect(() => {
54    const canvas = chartRef.current;
55    if (!canvas) return;
56
57    const ctx = canvas.getContext('2d');
58    const width = canvas.width;
59    const height = canvas.height;
60
61    // Nettoyage du canvas
62    ctx.clearRect(0, 0, width, height);
63
64    // Dessin du graphique
65    ctx.fillStyle = 'rgba(97, 218, 251, 0.3)';
66    ctx.strokeStyle = '#61dafb';
67    ctx.lineWidth = 2;
68
69    const barWidth = width / data.length - 10;
70    data.forEach((value, index) => {
71      const x = index * (barWidth + 10) + 5;
72      const barHeight = (value / 100) * height;
73      const y = height - barHeight;
74
75      ctx.fillRect(x, y, barWidth, barHeight);
76      ctx.strokeRect(x, y, barWidth, barHeight);
77    });
78
79    // Cleanup - nettoyage du canvas au démontage
80    return () => {
81      ctx.clearRect(0, 0, width, height);
82    };
83  }, [data]);
84
85  const updateData = () => {
86    setData(prev => prev.map(() => Math.random() * 100));
87  };
88
89  return (
90    <div className="chart-sync">
91      <h2>Synchronisation avec Canvas</h2>
92      <canvas ref={chartRef} width={350} height={200} />
93      <button onClick={updateData}>Rafraîchir les données</button>
94    </div>
95  );
96}
97
98// Synchronisation avec WebSocket (simulation)
99function WebSocketSync() {
100  const [messages, setMessages] = useState([]);
101  const [connected, setConnected] = useState(false);
102
103  useEffect(() => {
104    // Simulation de la connexion WebSocket
105    console.log('Connecting to WebSocket...');
106    setConnected(true);
107
108    const interval = setInterval(() => {
109      const newMessage = {
110        id: Date.now(),
111        text: `Message depuis l'espace #${Math.floor(Math.random() * 1000)}`,
112        time: new Date().toLocaleTimeString()
113      };
114      setMessages(prev => [...prev.slice(-4), newMessage]);
115    }, 2000);
116
117    // Cleanup - fermeture de la connexion
118    return () => {
119      console.log('Closing WebSocket connection...');
120      clearInterval(interval);
121      setConnected(false);
122    };
123  }, []);
124
125  return (
126    <div className="websocket-sync">
127      <h2>Communication WebSocket</h2>
128      <div className="connection-status">
129        {connected ? 'Connecté' : 'Déconnecté'}
130      </div>
131      <div className="messages">
132        {messages.map(msg => (
133          <div key={msg.id} className="message">
134            <span className="time">[{msg.time}]</span> {msg.text}
135          </div>
136        ))}
137      </div>
138    </div>
139  );
140}
141
142function App() {
143  return (
144    <div className="app">
145      <h1>Synchronisation avec des systèmes externes</h1>
146      <div className="demos">
147        <SpaceNavigationSystem />
148        <ExternalChartSync />
149        <WebSocketSync />
150      </div>
151    </div>
152  );
153}
154
155export default App;

Tu as repéré une erreur dans cette leçon ?

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les variantes de useEffect du déclenchement le plus fréquent au moins fréquent :

  • Éditeur de code

    Crée [seconds, setSeconds] = useState(0). useEffect avec setInterval toutes les 1s : setSeconds(s => s + 1). IMPORTANT : return () => clearInterval(intervalId) - le nettoyage prévient les fuites de mémoire ! Affiche : 'Temps de mission : {seconds}s'

  • Tri vertical

    Ordonne les étapes du cycle de vie d'un effet avec une fonction de nettoyage :

  • Clique dans l'ordre

    Ordonne la structure du composant avec useEffect :

Articles utiles