Kurs JavaScript i React · Moduł 5: useEffect i własne hooki
Synchronizacja z zewnętrznymi systemami
W tej lekcji7
W poprzednim module poznaliśmy podstawy efektów ubocznych z hookiem useEffect. Teraz zagłębimy się w jeden z najważniejszych przypadków użycia efektów: synchronizację komponentów React z zewnętrznymi systemami i bibliotekami.
Dlaczego synchronizacja jest ważna?
Komponenty React istnieją w swoim reaktywnym środowisku, ale często muszą współpracować z niezależnymi systemami, które nie są świadome cyklu życia React ani jego modelu renderowania. Możesz wyobrazić sobie to jako mostek między statkiem kosmicznym React a innymi systemami - komunikacja musi być dwustronna i niezawodna.
Przykładami takich zewnętrznych systemów są:
- Biblioteki map (np. Google Maps, Leaflet)
- Biblioteki wykresów (np. D3.js, Chart.js)
- Odtwarzacze mediów (np. HTML5 Video, Web Audio API)
- SDK do websocketów i komunikacji czasu rzeczywistego
- Systemy zarządzania formularzami (np. Formik, React Hook Form)
- Biblioteki animacji (np. GSAP, Anime.js)
- Interfejsy przeglądarki (geolokalizacja, Web Bluetooth)
Podstawowy wzorzec synchronizacji
Podstawowy wzorzec synchronizacji z zewnętrznym systemem wygląda następująco:
- Inicjalizuj zewnętrzną bibliotekę/system po pierwszym renderowaniu
- Aktualizuj zewnętrzny system, gdy zmieniają się odpowiednie props lub stan
- Sprzątaj po sobie (zatrzymaj, odłącz), gdy komponent jest odmontowywany
1function ExternalSystemComponent({ config }) {
2 useEffect(() => {
3 // 1. Inicjalizacja
4 const externalInstance = new ExternalSystem(config);
5 externalInstance.initialize();
6
7 // 2. Aktualizacja (opcjonalnie)
8 externalInstance.update(config);
9
10 // 3. Sprzątanie
11 return () => {
12 externalInstance.cleanup();
13 };
14 }, [config]); // Ponowna synchronizacja, gdy zmienia się konfiguracja
15
16 return <div className="external-system-container" />;
17}Praktyczne przykłady synchronizacji
1. Integracja z biblioteką map (Leaflet)
Leaflet to popularna biblioteka do interaktywnych map, która działa niezależnie od React. Oto przykład integracji:
1import React, { useEffect, useRef } from 'react';
2import L from 'leaflet';
3import 'leaflet/dist/leaflet.css';
4
5function MarsExplorationMap({ missionCoordinates, roverPosition, zoom = 5 }) {
6 // Ref do kontenera mapy
7 const mapContainerRef = useRef(null);
8 // Ref do instancji mapy (aby uniknąć ponownego tworzenia)
9 const mapInstanceRef = useRef(null);
10 // Ref do markera łazika (aby aktualizować tylko jego pozycję)
11 const roverMarkerRef = useRef(null);
12
13 // Inicjalizacja mapy
14 useEffect(() => {
15 if (!mapInstanceRef.current && mapContainerRef.current) {
16 // Tworzenie instancji mapy tylko raz
17 const mapInstance = L.map(mapContainerRef.current).setView(
18 [missionCoordinates.lat, missionCoordinates.lng],
19 zoom
20 );
21
22 // Dodanie warstwy mapy
23 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
24 attribution: '© OpenStreetMap contributors'
25 }).addTo(mapInstance);
26
27 // Zapisanie instancji mapy w referencji
28 mapInstanceRef.current = mapInstance;
29
30 // Dodanie markera łazika
31 roverMarkerRef.current = L.marker([roverPosition.lat, roverPosition.lng])
32 .addTo(mapInstance)
33 .bindPopup('Łazik marsjański');
34 }
35
36 // Funkcja czyszcząca
37 return () => {
38 if (mapInstanceRef.current) {
39 // Usunięcie mapy przy odmontowaniu komponentu
40 mapInstanceRef.current.remove();
41 mapInstanceRef.current = null;
42 roverMarkerRef.current = null;
43 }
44 };
45 }, []); // Pusta tablica zależności - inicjalizacja tylko raz
46
47 // Aktualizacja pozycji łazika
48 useEffect(() => {
49 if (roverMarkerRef.current && roverPosition) {
50 // Aktualizacja pozycji istniejącego markera
51 roverMarkerRef.current.setLatLng([roverPosition.lat, roverPosition.lng]);
52 }
53 }, [roverPosition]); // Aktualizuj tylko gdy zmienia się pozycja łazika
54
55 // Aktualizacja poziomu przybliżenia
56 useEffect(() => {
57 if (mapInstanceRef.current) {
58 mapInstanceRef.current.setZoom(zoom);
59 }
60 }, [zoom]); // Aktualizuj tylko gdy zmienia się zoom
61
62 return <div ref={mapContainerRef} style={{ height: '500px', width: '100%' }} />;
63}W powyższym przykładzie:
- Używamy referencji (useRef) do przechowywania elementów DOM i instancji biblioteki zewnętrznej
- Inicjalizujemy mapę tylko raz i zapisujemy jej instancję
- Używamy oddzielnych efektów do obsługi różnych aspektów synchronizacji
- Czyścimy zasoby, gdy komponent jest odmontowywany
2. Integracja z biblioteką wykresów (Chart.js)
Chart.js to popularna biblioteka do tworzenia interaktywnych wykresów:
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 // Upewniamy się, że mamy dostęp do elementu canvas
10 if (!chartRef.current) return;
11
12 // Pobierz kontekst canvas
13 const ctx = chartRef.current.getContext('2d');
14
15 // Zniszcz poprzednią instancję wykresu, jeśli istnieje
16 if (chartInstanceRef.current) {
17 chartInstanceRef.current.destroy();
18 }
19
20 // Utwórz nową instancję wykresu
21 chartInstanceRef.current = new Chart(ctx, {
22 type: 'bar',
23 data: {
24 labels: resources.map(resource => resource.name),
25 datasets: [{
26 label: title || 'Zasoby misji',
27 data: resources.map(resource => resource.level),
28 backgroundColor: resources.map(resource => {
29 // Kolorowanie słupków w zależności od poziomu zasobów
30 if (resource.level < 20) return 'rgba(255, 0, 0, 0.7)'; // Krytyczny
31 if (resource.level < 50) return 'rgba(255, 165, 0, 0.7)'; // Ostrzeżenie
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 // Sprzątanie
50 return () => {
51 if (chartInstanceRef.current) {
52 chartInstanceRef.current.destroy();
53 chartInstanceRef.current = null;
54 }
55 };
56 }, [resources, title]); // Ponowne tworzenie wykresu przy zmianie danych lub tytułu
57
58 return <canvas ref={chartRef} />;
59}3. Obsługa Web Sockets
Integracja z Web Sockets umożliwia komunikację czasu rzeczywistego:
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 // Inicjalizacja socketu
9 const socket = new WebSocket(`wss://mission-control.example.com/ws/${missionId}`);
10
11 // Konfiguracja zdarzeń
12 socket.onopen = () => {
13 setStatus('connected');
14 console.log(`Połączono z misją ${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('Błąd połączenia z centrum kontroli misji');
25 console.error('WebSocket error:', err);
26 };
27
28 socket.onclose = () => {
29 setStatus('disconnected');
30 console.log('Połączenie zamknięte');
31 };
32
33 // Funkcja czyszcząca
34 return () => {
35 // Zamknięcie połączenia przy odmontowaniu
36 if (socket.readyState === WebSocket.OPEN) {
37 socket.close();
38 }
39 };
40 }, [missionId, onMessage]); // Ponowne połączenie przy zmianie missionId
41
42 return (
43 <div className="mission-socket">
44 <div className={`connection-status ${status}`}>
45 Status połączenia: {status}
46 </div>
47 {error && <div className="connection-error">{error}</div>}
48 </div>
49 );
50}Strategie efektywnej synchronizacji
1. Minimalizacja ponownych inicjalizacji
Unikaj niepotrzebnego inicjalizowania zewnętrznych systemów, gdy tylko część konfiguracji się zmienia. Zamiast tego:
- Używaj osobnych efektów dla różnych aspektów synchronizacji
- Wykorzystuj referencje do przechowywania instancji zewnętrznych bibliotek
- Aktualizuj istniejące instancje zamiast tworzyć nowe
1// ŹLE - pojedynczy efekt reinikcjalizuje całość
2useEffect(() => {
3 const chart = new ChartLibrary(config);
4 return () => chart.destroy();
5}, [config]); // Każda zmiana w config spowoduje reinicjalizację
6
7// DOBRZE - oddzielna inicjalizacja i aktualizacja
8// Ref do przechowywania instancji
9const chartRef = useRef(null);
10
11// Inicjalizacja tylko raz
12useEffect(() => {
13 chartRef.current = new ChartLibrary(containerRef.current);
14 return () => chartRef.current.destroy();
15}, []);
16
17// Aktualizacja konfiguracji bez reinicjalizacji
18useEffect(() => {
19 if (chartRef.current) {
20 chartRef.current.updateConfig(config);
21 }
22}, [config]);2. Zarządzanie subskrypcjami
Przy pracy z zewnętrznymi źródłami danych, które używają subskrypcji:
- Zawsze anuluj subskrypcje w funkcji czyszczącej
- Aktualizuj subskrypcje, gdy zmieniają się parametry
1function TelemetryMonitor({ satelliteId, dataType }) {
2 const [data, setData] = useState(null);
3
4 useEffect(() => {
5 // Utworzenie subskrypcji
6 const subscription = telemetryAPI.subscribe(
7 satelliteId,
8 dataType,
9 (newData) => setData(newData)
10 );
11
12 // Czyszczenie
13 return () => subscription.unsubscribe();
14 }, [satelliteId, dataType]); // Ponowna subskrypcja gdy zmieni się ID lub typ danych
15
16 return <div>{/* Renderowanie danych */}</div>;
17}3. Opóźnione inicjalizowanie zasobożernych bibliotek
Dla ciężkich bibliotek zewnętrznych, możesz opóźnić ich inicjalizację do momentu, gdy są faktycznie potrzebne:
1function HeavyChart({ data, shouldRender }) {
2 const [chartInstance, setChartInstance] = useState(null);
3 const containerRef = useRef(null);
4
5 // Leniwa inicjalizacja
6 useEffect(() => {
7 if (!shouldRender) {
8 // Nie inicjalizuj jeśli nie powinniśmy jeszcze renderować
9 return;
10 }
11
12 // Dynamiczny import ciężkiej biblioteki
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]); // Uruchom tylko gdy shouldRender zmienia się na true
22
23 // Aktualizacja danych
24 useEffect(() => {
25 if (chartInstance && data) {
26 chartInstance.updateData(data);
27 }
28 }, [chartInstance, data]);
29
30 return <div ref={containerRef} />;
31}Tworzenie własnych hooków do synchronizacji
Aby poprawić ponowne użycie kodu i enkapsulację logiki synchronizacji, warto tworzyć własne hooki.
Przykład: Custom hook do integracji z mapą
1function useLeafletMap(containerRef, initialConfig) {
2 const mapRef = useRef(null);
3 const [isReady, setIsReady] = useState(false);
4
5 // Inicjalizacja
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 // Metody do manipulacji mapą
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// Użycie custom hooka
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 // Dodanie markera po załadowaniu mapy
53 useEffect(() => {
54 if (isReady) {
55 const marker = addMarker([24.7136, 46.6753], { title: 'Baza Alfa' });
56 marker.bindPopup('Baza Alfa - Główna kolonia na Marsie').openPopup();
57 }
58 }, [isReady, addMarker]);
59
60 return (
61 <div>
62 <h2>Mapa kolonii marsjańskiej</h2>
63 <div ref={containerRef} style={{ height: '500px' }} />
64 {!isReady && <div>Ładowanie mapy...</div>}
65 </div>
66 );
67}Przykład: Custom hook do lokalnego składowania
1function useLocalStorage(key, initialValue) {
2 // Funkcja inicjalizująca stan - uruchamiana tylko raz
3 const [storedValue, setStoredValue] = useState(() => {
4 try {
5 // Sprawdzenie czy localStorage jest dostępny
6 if (typeof window === 'undefined') {
7 return initialValue;
8 }
9
10 // Próba pobrania wartości z localStorage
11 const item = window.localStorage.getItem(key);
12 return item ? JSON.parse(item) : initialValue;
13 } catch (error) {
14 console.error('Błąd odczytu z localStorage:', error);
15 return initialValue;
16 }
17 });
18
19 // Funkcja aktualizująca wartość w localStorage
20 const setValue = useCallback((value) => {
21 try {
22 // Obsługa funkcji jako wartości (jak w setState)
23 const valueToStore = value instanceof Function ? value(storedValue) : value;
24
25 // Aktualizacja stanu React
26 setStoredValue(valueToStore);
27
28 // Aktualizacja localStorage
29 if (typeof window !== 'undefined') {
30 window.localStorage.setItem(key, JSON.stringify(valueToStore));
31 }
32 } catch (error) {
33 console.error('Błąd zapisu do localStorage:', error);
34 }
35 }, [key, storedValue]);
36
37 // Synchronizacja z localStorage przy zmianie klucza
38 useEffect(() => {
39 // Pobranie aktualnej wartości dla nowego klucza
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('Błąd synchronizacji z localStorage:', error);
47 }
48 }, [key, initialValue]);
49
50 return [storedValue, setValue];
51}
52
53// Użycie custom hooka
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>Ustawienia astronauty</h2>
71 <button onClick={toggleDarkMode}>
72 Przełącz na tryb {settings.darkMode ? 'jasny' : 'ciemny'}
73 </button>
74 <div>
75 Powiadomienie o poziomie tlenu: {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 Włącz powiadomienia
97 </label>
98 </div>
99 );
100}Obsługa efektów w testach
Testowanie komponentów, które synchronizują się z zewnętrznymi systemami, wymaga specjalnego podejścia:
- Mockowanie zewnętrznych bibliotek
- Testowanie efektów czyszczących
- Sprawdzanie, czy synchronizacja działa poprawnie
Przykład testu dla komponentu z WebSocket:
1import { render, act, screen, waitFor } from '@testing-library/react';
2import MissionControlSocket from './MissionControlSocket';
3
4// Mock dla WebSocket
5class MockWebSocket {
6 constructor(url) {
7 this.url = url;
8 this.readyState = WebSocket.CONNECTING;
9
10 // Automatyczne "połączenie" po utworzeniu
11 setTimeout(() => {
12 this.readyState = WebSocket.OPEN;
13 this.onopen && this.onopen();
14 }, 0);
15 }
16
17 // Metody do symulowania zdarzeń z zewnątrz
18 simulateMessage(data) {
19 this.onmessage && this.onmessage({ data: JSON.stringify(data) });
20 }
21
22 simulateError() {
23 this.onerror && this.onerror(new Error('Test error'));
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// Podmiana globalnej klasy WebSocket
37global.WebSocket = MockWebSocket;
38
39describe('MissionControlSocket', () => {
40 test('should connect to WebSocket and handle messages', async () => {
41 const mockOnMessage = jest.fn();
42
43 render(
44 <MissionControlSocket
45 missionId="apollo11"
46 onMessage={mockOnMessage}
47 />
48 );
49
50 // Czekaj na połączenie
51 await waitFor(() => {
52 expect(screen.getByText(/status połączenia: connected/i)).toBeInTheDocument();
53 });
54
55 // Symulacja otrzymania wiadomości
56 const testData = { type: 'telemetry', oxygen: 95, fuel: 76 };
57 act(() => {
58 // Dostęp do instancji WebSocket przez monkeypatching
59 const socket = global.WebSocket.instances[0];
60 socket.simulateMessage(testData);
61 });
62
63 // Sprawdzenie czy callback został wywołany z odpowiednimi danymi
64 expect(mockOnMessage).toHaveBeenCalledWith(testData);
65
66 // Symulacja błędu
67 act(() => {
68 const socket = global.WebSocket.instances[0];
69 socket.simulateError();
70 });
71
72 // Sprawdzenie czy status błędu jest widoczny
73 expect(screen.getByText(/status połączenia: error/i)).toBeInTheDocument();
74 expect(screen.getByText(/błąd połączenia/i)).toBeInTheDocument();
75 });
76
77 test('should clean up WebSocket connection on unmount', () => {
78 const { unmount } = render(
79 <MissionControlSocket missionId="apollo11" onMessage={() => {}} />
80 );
81
82 // Pobierz instancję WebSocket
83 const socket = global.WebSocket.instances[0];
84 const closeSpy = jest.spyOn(socket, 'close');
85
86 // Odmontowanie komponentu
87 unmount();
88
89 // Sprawdzenie czy socket został zamknięty
90 expect(closeSpy).toHaveBeenCalled();
91 });
92});Podsumowanie
Synchronizacja z zewnętrznymi systemami to jedna z najważniejszych funkcji hooka useEffect. Umożliwia wykorzystanie istniejących bibliotek i API w ekosystemie React, zachowując reaktywny model aktualizacji.
Najważniejsze zasady:
- Inicjalizuj zewnętrzne systemy po pierwszym renderowaniu
- Aktualizuj je, gdy zmieniają się odpowiednie dane
- Sprzątaj zasoby, gdy komponent jest odmontowywany
- Dziel efekty według ich odpowiedzialności
- Enkapsuluj logikę synchronizacji w dedykowanych hookach
- Minimalizuj zbędne reinicjalizacje
- Testuj poprawność synchronizacji i czyszczenia
Pamiętaj, że głównym celem synchronizacji jest utrzymanie spójności między reaktywnym światem React a zewnętrznymi systemami, które nie wiedzą nic o cyklu życia komponentów. Dzięki prawidłowej synchronizacji, Twoja aplikacja będzie działać płynnie i efektywnie, łącząc różne technologie w jedną, spójną całość.
Kod do tej lekcji: App.jsx
1import React, { useState, useEffect, useRef } from 'react';
2
3// Synchronizacja z zewnętrznym API - system nawigacji kosmicznej
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 // Synchronizacja z "zewnętrznym" systemem nawigacji
10 useEffect(() => {
11 if (!isTracking) return;
12
13 // Symulacja zewnętrznego systemu nawigacji
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 - rozłączenie z systemem
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>System Nawigacji Kosmicznej</h2>
32 <div className="status">
33 Status: {isTracking ? 'Aktywny' : 'Nieaktywny'}
34 </div>
35 <div className="coordinates">
36 <p>Współrzędne X: {position.x.toFixed(2)}</p>
37 <p>Współrzędne Y: {position.y.toFixed(2)}</p>
38 </div>
39 {lastSync && <p className="sync-time">Ostatnia synchronizacja: {lastSync}</p>}
40 <button onClick={() => setIsTracking(!isTracking)}>
41 {isTracking ? 'Zatrzymaj śledzenie' : 'Wznów śledzenie'}
42 </button>
43 </div>
44 );
45}
46
47// Synchronizacja z DOM - zewnętrzna biblioteka wykresów
48function ExternalChartSync() {
49 const chartRef = useRef(null);
50 const [data, setData] = useState([30, 50, 40, 60, 45, 70, 55]);
51
52 // Synchronizacja z "zewnętrzną biblioteką wykresów"
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 // Czyszczenie canvas
62 ctx.clearRect(0, 0, width, height);
63
64 // Rysowanie wykresu
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 - czyszczenie canvas przy odmontowaniu
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>Synchronizacja z Canvas</h2>
92 <canvas ref={chartRef} width={350} height={200} />
93 <button onClick={updateData}>Odśwież dane</button>
94 </div>
95 );
96}
97
98// Synchronizacja z WebSocket (symulacja)
99function WebSocketSync() {
100 const [messages, setMessages] = useState([]);
101 const [connected, setConnected] = useState(false);
102
103 useEffect(() => {
104 // Symulacja połączenia WebSocket
105 console.log('Connecting to WebSocket...');
106 setConnected(true);
107
108 const interval = setInterval(() => {
109 const newMessage = {
110 id: Date.now(),
111 text: `Wiadomość z kosmosu #${Math.floor(Math.random() * 1000)}`,
112 time: new Date().toLocaleTimeString()
113 };
114 setMessages(prev => [...prev.slice(-4), newMessage]);
115 }, 2000);
116
117 // Cleanup - zamknięcie połączenia
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>Komunikacja WebSocket</h2>
128 <div className="connection-status">
129 {connected ? 'Połączono' : 'Rozłączono'}
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>Synchronizacja z zewnętrznymi systemami</h1>
146 <div className="demos">
147 <SpaceNavigationSystem />
148 <ExternalChartSync />
149 <WebSocketSync />
150 </div>
151 </div>
152 );
153}
154
155export default App;Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj warianty useEffect od najczęściej do najrzadziej uruchamianego:
- Edytor kodu
Stwórz [seconds, setSeconds] = useState(0). useEffect z setInterval co 1s: setSeconds(s => s + 1). WAŻNE: return () => clearInterval(intervalId) - cleanup zapobiega wyciekom pamięci! Wyświetl: 'Czas misji: {seconds}s'
- Układanie w pionie
Uporządkuj etapy cyklu życia efektu z funkcją cleanup:
- Klikanie w kolejności
Uporządkuj strukturę komponentu z useEffect: