JavaScript and React course ยท Module 5: useEffect and Custom Hooks
Synchronization with External Systems
In this lesson7
In the previous module, we learned the basics of side effects with the useEffect hook. Now we'll dive deeper into one of the most important use cases for effects: synchronizing React components with external systems and libraries.
Why is synchronization important?
React components exist in their own reactive environment, but they often need to interact with independent systems that are unaware of React's lifecycle or rendering model. You can think of this as a bridge between the React spaceship and other systems - communication must be bidirectional and reliable.
Examples of such external systems include:
- Map libraries (e.g., Google Maps, Leaflet)
- Chart libraries (e.g., D3.js, Chart.js)
- Media players (e.g., HTML5 Video, Web Audio API)
- WebSocket SDKs and real-time communication
- Form management systems (e.g., Formik, React Hook Form)
- Animation libraries (e.g., GSAP, Anime.js)
- Browser interfaces (geolocation, Web Bluetooth)
Basic synchronization pattern
The basic pattern for synchronization with an external system looks as follows:
- Initialize the external library/system after the first render
- Update the external system when relevant props or state change
- Clean up (stop, disconnect) when the component is unmounted
1function ExternalSystemComponent({ config }) {
2 useEffect(() => {
3 // 1. Initialization
4 const externalInstance = new ExternalSystem(config);
5 externalInstance.initialize();
6
7 // 2. Update (optional)
8 externalInstance.update(config);
9
10 // 3. Cleanup
11 return () => {
12 externalInstance.cleanup();
13 };
14 }, [config]); // Re-synchronize when config changes
15
16 return <div className="external-system-container" />;
17}Practical synchronization examples
1. Integration with a map library (Leaflet)
Leaflet is a popular library for interactive maps that works independently from React. Here's an integration example:
1import React, { useEffect, useRef } from 'react';
2import L from 'leaflet';
3import 'leaflet/dist/leaflet.css';
4
5function MarsExplorationMap({ missionCoordinates, roverPosition, zoom = 5 }) {
6 // Ref to the map container
7 const mapContainerRef = useRef(null);
8 // Ref to the map instance (to avoid re-creation)
9 const mapInstanceRef = useRef(null);
10 // Ref to the rover marker (to update only its position)
11 const roverMarkerRef = useRef(null);
12
13 // Map initialization
14 useEffect(() => {
15 if (!mapInstanceRef.current && mapContainerRef.current) {
16 // Create map instance only once
17 const mapInstance = L.map(mapContainerRef.current).setView(
18 [missionCoordinates.lat, missionCoordinates.lng],
19 zoom
20 );
21
22 // Add map layer
23 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
24 attribution: 'ยฉ OpenStreetMap contributors'
25 }).addTo(mapInstance);
26
27 // Save map instance in reference
28 mapInstanceRef.current = mapInstance;
29
30 // Add rover marker
31 roverMarkerRef.current = L.marker([roverPosition.lat, roverPosition.lng])
32 .addTo(mapInstance)
33 .bindPopup('Mars Rover');
34 }
35
36 // Cleanup function
37 return () => {
38 if (mapInstanceRef.current) {
39 // Remove map when component unmounts
40 mapInstanceRef.current.remove();
41 mapInstanceRef.current = null;
42 roverMarkerRef.current = null;
43 }
44 };
45 }, []); // Empty dependency array - initialize only once
46
47 // Update rover position
48 useEffect(() => {
49 if (roverMarkerRef.current && roverPosition) {
50 // Update the position of the existing marker
51 roverMarkerRef.current.setLatLng([roverPosition.lat, roverPosition.lng]);
52 }
53 }, [roverPosition]); // Update only when rover position changes
54
55 // Update zoom level
56 useEffect(() => {
57 if (mapInstanceRef.current) {
58 mapInstanceRef.current.setZoom(zoom);
59 }
60 }, [zoom]); // Update only when zoom changes
61
62 return <div ref={mapContainerRef} style={{ height: '500px', width: '100%' }} />;
63}In the example above:
- We use references (useRef) to store DOM elements and external library instances
- We initialize the map only once and save its instance
- We use separate effects to handle different aspects of synchronization
- We clean up resources when the component is unmounted
2. Integration with a chart library (Chart.js)
Chart.js is a popular library for creating interactive charts:
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 // Make sure we have access to the canvas element
10 if (!chartRef.current) return;
11
12 // Get canvas context
13 const ctx = chartRef.current.getContext('2d');
14
15 // Destroy previous chart instance if it exists
16 if (chartInstanceRef.current) {
17 chartInstanceRef.current.destroy();
18 }
19
20 // Create new chart instance
21 chartInstanceRef.current = new Chart(ctx, {
22 type: 'bar',
23 data: {
24 labels: resources.map(resource => resource.name),
25 datasets: [{
26 label: title || 'Mission Resources',
27 data: resources.map(resource => resource.level),
28 backgroundColor: resources.map(resource => {
29 // Color bars based on resource level
30 if (resource.level < 20) return 'rgba(255, 0, 0, 0.7)'; // Critical
31 if (resource.level < 50) return 'rgba(255, 165, 0, 0.7)'; // Warning
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 // Cleanup
50 return () => {
51 if (chartInstanceRef.current) {
52 chartInstanceRef.current.destroy();
53 chartInstanceRef.current = null;
54 }
55 };
56 }, [resources, title]); // Recreate chart when data or title changes
57
58 return <canvas ref={chartRef} />;
59}3. Handling Web Sockets
Web Socket integration enables real-time communication:
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 // Socket initialization
9 const socket = new WebSocket(`wss://mission-control.example.com/ws/${missionId}`);
10
11 // Event configuration
12 socket.onopen = () => {
13 setStatus('connected');
14 console.log(`Connected to 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('Connection error with mission control center');
25 console.error('WebSocket error:', err);
26 };
27
28 socket.onclose = () => {
29 setStatus('disconnected');
30 console.log('Connection closed');
31 };
32
33 // Cleanup function
34 return () => {
35 // Close connection on unmount
36 if (socket.readyState === WebSocket.OPEN) {
37 socket.close();
38 }
39 };
40 }, [missionId, onMessage]); // Reconnect when missionId changes
41
42 return (
43 <div className="mission-socket">
44 <div className={`connection-status ${status}`}>
45 Connection status: {status}
46 </div>
47 {error && <div className="connection-error">{error}</div>}
48 </div>
49 );
50}Strategies for efficient synchronization
1. Minimizing re-initializations
Avoid unnecessarily re-initializing external systems when only part of the configuration changes. Instead:
- Use separate effects for different aspects of synchronization
- Use references to store instances of external libraries
- Update existing instances instead of creating new ones
1// BAD - single effect re-initializes everything
2useEffect(() => {
3 const chart = new ChartLibrary(config);
4 return () => chart.destroy();
5}, [config]); // Every change in config causes re-initialization
6
7// GOOD - separate initialization and update
8// Ref to store instance
9const chartRef = useRef(null);
10
11// Initialize only once
12useEffect(() => {
13 chartRef.current = new ChartLibrary(containerRef.current);
14 return () => chartRef.current.destroy();
15}, []);
16
17// Update configuration without re-initialization
18useEffect(() => {
19 if (chartRef.current) {
20 chartRef.current.updateConfig(config);
21 }
22}, [config]);2. Managing subscriptions
When working with external data sources that use subscriptions:
- Always cancel subscriptions in the cleanup function
- Update subscriptions when parameters change
1function TelemetryMonitor({ satelliteId, dataType }) {
2 const [data, setData] = useState(null);
3
4 useEffect(() => {
5 // Create subscription
6 const subscription = telemetryAPI.subscribe(
7 satelliteId,
8 dataType,
9 (newData) => setData(newData)
10 );
11
12 // Cleanup
13 return () => subscription.unsubscribe();
14 }, [satelliteId, dataType]); // Re-subscribe when ID or data type changes
15
16 return <div>{/* Rendering data */}</div>;
17}3. Lazy initialization of resource-heavy libraries
For heavy external libraries, you can delay their initialization until they are actually needed:
1function HeavyChart({ data, shouldRender }) {
2 const [chartInstance, setChartInstance] = useState(null);
3 const containerRef = useRef(null);
4
5 // Lazy initialization
6 useEffect(() => {
7 if (!shouldRender) {
8 // Don't initialize if we shouldn't render yet
9 return;
10 }
11
12 // Dynamic import of heavy library
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]); // Run only when shouldRender changes to true
22
23 // Update data
24 useEffect(() => {
25 if (chartInstance && data) {
26 chartInstance.updateData(data);
27 }
28 }, [chartInstance, data]);
29
30 return <div ref={containerRef} />;
31}Creating custom hooks for synchronization
To improve code reuse and encapsulate synchronization logic, it's worth creating custom hooks.
Example: Custom hook for map integration
1function useLeafletMap(containerRef, initialConfig) {
2 const mapRef = useRef(null);
3 const [isReady, setIsReady] = useState(false);
4
5 // Initialization
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 // Methods for manipulating the 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// Using the custom hook
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 // Add marker after map loads
53 useEffect(() => {
54 if (isReady) {
55 const marker = addMarker([24.7136, 46.6753], { title: 'Base Alpha' });
56 marker.bindPopup('Base Alpha - Main colony on Mars').openPopup();
57 }
58 }, [isReady, addMarker]);
59
60 return (
61 <div>
62 <h2>Mars Colony Map</h2>
63 <div ref={containerRef} style={{ height: '500px' }} />
64 {!isReady && <div>Loading map...</div>}
65 </div>
66 );
67}Example: Custom hook for local storage
1function useLocalStorage(key, initialValue) {
2 // State initializer function - runs only once
3 const [storedValue, setStoredValue] = useState(() => {
4 try {
5 // Check if localStorage is available
6 if (typeof window === 'undefined') {
7 return initialValue;
8 }
9
10 // Try to get value from localStorage
11 const item = window.localStorage.getItem(key);
12 return item ? JSON.parse(item) : initialValue;
13 } catch (error) {
14 console.error('Error reading from localStorage:', error);
15 return initialValue;
16 }
17 });
18
19 // Function to update value in localStorage
20 const setValue = useCallback((value) => {
21 try {
22 // Support function as value (like in setState)
23 const valueToStore = value instanceof Function ? value(storedValue) : value;
24
25 // Update React state
26 setStoredValue(valueToStore);
27
28 // Update localStorage
29 if (typeof window !== 'undefined') {
30 window.localStorage.setItem(key, JSON.stringify(valueToStore));
31 }
32 } catch (error) {
33 console.error('Error writing to localStorage:', error);
34 }
35 }, [key, storedValue]);
36
37 // Synchronize with localStorage when key changes
38 useEffect(() => {
39 // Get current value for new key
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('Error synchronizing with localStorage:', error);
47 }
48 }, [key, initialValue]);
49
50 return [storedValue, setValue];
51}
52
53// Using the custom hook
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>Astronaut Settings</h2>
71 <button onClick={toggleDarkMode}>
72 Switch to {settings.darkMode ? 'light' : 'dark'} mode
73 </button>
74 <div>
75 Oxygen level alert: {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 Enable notifications
97 </label>
98 </div>
99 );
100}Handling effects in tests
Testing components that synchronize with external systems requires a special approach:
- Mocking external libraries
- Testing cleanup functions
- Verifying that synchronization works correctly
Example test for a WebSocket component:
1import { render, act, screen, waitFor } from '@testing-library/react';
2import MissionControlSocket from './MissionControlSocket';
3
4// Mock for WebSocket
5class MockWebSocket {
6 constructor(url) {
7 this.url = url;
8 this.readyState = WebSocket.CONNECTING;
9
10 // Auto "connect" after creation
11 setTimeout(() => {
12 this.readyState = WebSocket.OPEN;
13 this.onopen && this.onopen();
14 }, 0);
15 }
16
17 // Methods for simulating external events
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// Replace global WebSocket class
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 // Wait for connection
51 await waitFor(() => {
52 expect(screen.getByText(/connection status: connected/i)).toBeInTheDocument();
53 });
54
55 // Simulate receiving a message
56 const testData = { type: 'telemetry', oxygen: 95, fuel: 76 };
57 act(() => {
58 // Access WebSocket instance through monkeypatching
59 const socket = global.WebSocket.instances[0];
60 socket.simulateMessage(testData);
61 });
62
63 // Check if callback was called with appropriate data
64 expect(mockOnMessage).toHaveBeenCalledWith(testData);
65
66 // Simulate error
67 act(() => {
68 const socket = global.WebSocket.instances[0];
69 socket.simulateError();
70 });
71
72 // Check if error status is visible
73 expect(screen.getByText(/connection status: error/i)).toBeInTheDocument();
74 expect(screen.getByText(/connection error/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 // Get WebSocket instance
83 const socket = global.WebSocket.instances[0];
84 const closeSpy = jest.spyOn(socket, 'close');
85
86 // Unmount the component
87 unmount();
88
89 // Check if socket was closed
90 expect(closeSpy).toHaveBeenCalled();
91 });
92});Summary
Synchronization with external systems is one of the most important functions of the useEffect hook. It enables the use of existing libraries and APIs within the React ecosystem while maintaining the reactive update model.
Key principles:
- Initialize external systems after the first render
- Update them when relevant data changes
- Clean up resources when the component is unmounted
- Separate effects by their responsibility
- Encapsulate synchronization logic in dedicated hooks
- Minimize unnecessary re-initializations
- Test the correctness of synchronization and cleanup
Remember that the main goal of synchronization is to maintain consistency between the reactive world of React and external systems that know nothing about the component lifecycle. With proper synchronization, your application will work smoothly and efficiently, connecting different technologies into one cohesive whole.
Code for this lesson: App.jsx
1import React, { useState, useEffect, useRef } from 'react';
2
3// Synchronization with an external API - space navigation system
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 // Synchronization with an "external" navigation system
10 useEffect(() => {
11 if (!isTracking) return;
12
13 // Simulating an external navigation system
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 - disconnecting from the system
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>Space Navigation System</h2>
32 <div className="status">
33 Status: {isTracking ? 'Active' : 'Inactive'}
34 </div>
35 <div className="coordinates">
36 <p>Coordinate X: {position.x.toFixed(2)}</p>
37 <p>Coordinate Y: {position.y.toFixed(2)}</p>
38 </div>
39 {lastSync && <p className="sync-time">Last synchronization: {lastSync}</p>}
40 <button onClick={() => setIsTracking(!isTracking)}>
41 {isTracking ? 'Stop tracking' : 'Resume tracking'}
42 </button>
43 </div>
44 );
45}
46
47// Synchronization with DOM - external charting library
48function ExternalChartSync() {
49 const chartRef = useRef(null);
50 const [data, setData] = useState([30, 50, 40, 60, 45, 70, 55]);
51
52 // Synchronization with an "external charting library"
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 // Clearing the canvas
62 ctx.clearRect(0, 0, width, height);
63
64 // Drawing the chart
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 - clearing the canvas on unmount
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>Canvas Synchronization</h2>
92 <canvas ref={chartRef} width={350} height={200} />
93 <button onClick={updateData}>Refresh data</button>
94 </div>
95 );
96}
97
98// Synchronization with WebSocket (simulation)
99function WebSocketSync() {
100 const [messages, setMessages] = useState([]);
101 const [connected, setConnected] = useState(false);
102
103 useEffect(() => {
104 // Simulating a WebSocket connection
105 console.log('Connecting to WebSocket...');
106 setConnected(true);
107
108 const interval = setInterval(() => {
109 const newMessage = {
110 id: Date.now(),
111 text: `Message from space #${Math.floor(Math.random() * 1000)}`,
112 time: new Date().toLocaleTimeString()
113 };
114 setMessages(prev => [...prev.slice(-4), newMessage]);
115 }, 2000);
116
117 // Cleanup - closing the connection
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>WebSocket Communication</h2>
128 <div className="connection-status">
129 {connected ? 'Connected' : 'Disconnected'}
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>Synchronization with external systems</h1>
146 <div className="demos">
147 <SpaceNavigationSystem />
148 <ExternalChartSync />
149 <WebSocketSync />
150 </div>
151 </div>
152 );
153}
154
155export default App;Spotted a mistake in this lesson?
Hands-on tasks in the game
- Vertical ordering
Order useEffect variants from most frequently to least frequently triggered:
- Code editor
Create [seconds, setSeconds] = useState(0). useEffect with setInterval every 1s: setSeconds(s => s + 1). IMPORTANT: return () => clearInterval(intervalId) - cleanup prevents memory leaks! Display: 'Mission time: {seconds}s'
- Vertical ordering
Order the stages of the effect lifecycle with a cleanup function:
- Click in order
Arrange the component structure with useEffect: