Cours JavaScript et React · Module 5: useEffect et hooks personnalisés
Synchronisation avec des systèmes externes
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 :
- Initialise la bibliothèque/le système externe après le premier rendu
- Mets à jour le système externe lorsque les props ou l'état pertinents changent
- 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 :
- Nous utilisons des références (useRef) pour stocker les éléments DOM et les instances de la bibliothèque externe
- Nous initialisons la carte une seule fois et sauvegardons son instance
- Nous utilisons des effets distincts pour gérer différents aspects de la synchronisation
- 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 :
- Utilise des effets distincts pour différents aspects de la synchronisation
- Utilise des références pour stocker les instances des bibliothèques externes
- 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 :
- Annule toujours les abonnements dans la fonction de nettoyage
- 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 :
- Simuler (mock) les bibliothèques externes
- Tester les fonctions de nettoyage
- 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 :
- Initialise les systèmes externes après le premier rendu
- Mets à jour ces systèmes lorsque les données pertinentes changent
- Nettoie les ressources lorsque le composant est démonté
- Sépare les effets selon leur responsabilité
- Encapsule la logique de synchronisation dans des hooks dédiés
- Minimise les ré-initialisations inutiles
- 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 :