Communicating with external APIs is one of the most common use cases for side effects in React applications. In this module, we'll learn how to efficiently fetch, process, and manage data from external sources.
In web applications, data often comes from external APIs that provide information in JSON, XML, or other formats. On our cosmic journey, we can imagine a mission control center fetching data from various satellites, spaceships, and research stations.
1import React, { useState, useEffect } from 'react';
2
3function AstronautsList() {
4 const [astronauts, setAstronauts] = useState([]);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7
8 useEffect(() => {
9 // Async function for fetching data
10 async function fetchAstronauts() {
11 try {
12 setLoading(true);
13 // Fetching data
14 const response = await fetch('https://api.spaceagency.com/astronauts');
15
16 // Check if response is OK
17 if (!response.ok) {
18 throw new Error(`HTTP Error: ${response.status}`);
19 }
20
21 // Parse JSON response
22 const data = await response.json();
23
24 // Update state
25 setAstronauts(data);
26 setError(null);
27 } catch (err) {
28 // Error handling
29 setError(`Failed to fetch data: ${err.message}`);
30 setAstronauts([]);
31 } finally {
32 // Mark loading as complete
33 setLoading(false);
34 }
35 }
36
37 // Call the function
38 fetchAstronauts();
39 }, []); // Empty dependency array - execute only once
40
41 // Render based on state
42 if (loading) return <div>Loading data...</div>;
43 if (error) return <div>Error: {error}</div>;
44
45 return (
46 <div>
47 <h2>Active Astronauts</h2>
48 <ul>
49 {astronauts.map(astronaut => (
50 <li key={astronaut.id}>
51 {astronaut.name} - {astronaut.role}
52 </li>
53 ))}
54 </ul>
55 </div>
56 );
57}In the example above:
useState to store data, loading state, and errorsuseEffect with an empty dependency array to fetch data only once after mountingasync/await to handle asynchronous data fetchingOften we need to fetch data based on parameters that can change:
1function MissionDetails({ missionId }) {
2 const [mission, setMission] = useState(null);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState(null);
5
6 useEffect(() => {
7 let isMounted = true; // Flag to prevent state updates after unmount
8
9 async function fetchMission() {
10 try {
11 setLoading(true);
12
13 const response = await fetch(
14 `https://api.spaceagency.com/missions/${missionId}`
15 );
16
17 if (!response.ok) {
18 throw new Error(`HTTP Error: ${response.status}`);
19 }
20
21 const data = await response.json();
22
23 // Update state only if component is still mounted
24 if (isMounted) {
25 setMission(data);
26 setError(null);
27 }
28 } catch (err) {
29 if (isMounted) {
30 setError(`Failed to fetch mission data: ${err.message}`);
31 setMission(null);
32 }
33 } finally {
34 if (isMounted) {
35 setLoading(false);
36 }
37 }
38 }
39
40 fetchMission();
41
42 // Cleanup function
43 return () => {
44 isMounted = false; // Mark that the component has been unmounted
45 };
46 }, [missionId]); // Refetch data when missionId changes
47
48 // Rendering
49 if (loading) return <div>Loading mission data...</div>;
50 if (error) return <div>Error: {error}</div>;
51 if (!mission) return <div>Mission not found</div>;
52
53 return (
54 <div className="mission-details">
55 <h2>{mission.name}</h2>
56 <p>Status: {mission.status}</p>
57 <p>Launch date: {new Date(mission.launchDate).toLocaleDateString()}</p>
58 <p>Commander: {mission.commander}</p>
59 {/* More mission details */}
60 </div>
61 );
62}In this example:
missionId, which can changeisMounted flag to prevent state updates after unmountisMounted = false on unmountThe Fetch API doesn't have a built-in cancellation mechanism, but we can use AbortController to abort a request:
1function SpaceWeather({ location }) {
2 const [weatherData, setWeatherData] = useState(null);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState(null);
5
6 useEffect(() => {
7 // Create AbortController to cancel the request
8 const abortController = new AbortController();
9 const signal = abortController.signal;
10
11 async function fetchWeatherData() {
12 try {
13 setLoading(true);
14
15 const response = await fetch(
16 `https://api.spaceagency.com/weather/${location}`,
17 { signal } // Assign signal to the request
18 );
19
20 if (!response.ok) {
21 throw new Error(`HTTP Error: ${response.status}`);
22 }
23
24 const data = await response.json();
25 setWeatherData(data);
26 setError(null);
27 } catch (err) {
28 // Ignore cancellation errors
29 if (err.name === 'AbortError') {
30 console.log('Weather request was cancelled');
31 } else {
32 setError(`Failed to fetch weather data: ${err.message}`);
33 setWeatherData(null);
34 }
35 } finally {
36 setLoading(false);
37 }
38 }
39
40 fetchWeatherData();
41
42 // Cleanup function cancels the request if the component
43 // is unmounted before fetching completes
44 return () => {
45 abortController.abort();
46 };
47 }, [location]);
48
49 // Rendering
50 if (loading) return <div>Loading weather data...</div>;
51 if (error) return <div>Error: {error}</div>;
52 if (!weatherData) return <div>No weather data available</div>;
53
54 return (
55 <div className="space-weather">
56 <h2>Space Weather: {location}</h2>
57 <p>Radiation: {weatherData.radiation} mSv</p>
58 <p>Solar activity: {weatherData.solarActivity}</p>
59 <p>Temperature: {weatherData.temperature}°C</p>
60 </div>
61 );
62}In this example:
AbortController to create a signal that can abort a Fetch requestabortController.abort() to cancel an ongoing requestAbortError that is thrown when a request is cancelledSometimes we need to fetch data from several sources simultaneously:
1function MissionDashboard({ missionId }) {
2 const [missionData, setMissionData] = useState({
3 details: null,
4 crew: [],
5 telemetry: null
6 });
7 const [loading, setLoading] = useState(true);
8 const [error, setError] = useState(null);
9
10 useEffect(() => {
11 let isMounted = true;
12
13 async function fetchMissionData() {
14 try {
15 setLoading(true);
16
17 // Fetch data in parallel using Promise.all
18 const [detailsResponse, crewResponse, telemetryResponse] = await Promise.all([
19 fetch(`https://api.spaceagency.com/missions/${missionId}`),
20 fetch(`https://api.spaceagency.com/missions/${missionId}/crew`),
21 fetch(`https://api.spaceagency.com/missions/${missionId}/telemetry`)
22 ]);
23
24 // Check responses
25 if (!detailsResponse.ok || !crewResponse.ok || !telemetryResponse.ok) {
26 throw new Error('One or more requests failed');
27 }
28
29 // Parse data in parallel
30 const [details, crew, telemetry] = await Promise.all([
31 detailsResponse.json(),
32 crewResponse.json(),
33 telemetryResponse.json()
34 ]);
35
36 if (isMounted) {
37 setMissionData({ details, crew, telemetry });
38 setError(null);
39 }
40 } catch (err) {
41 if (isMounted) {
42 setError(`Failed to fetch mission data: ${err.message}`);
43 }
44 } finally {
45 if (isMounted) {
46 setLoading(false);
47 }
48 }
49 }
50
51 fetchMissionData();
52
53 return () => {
54 isMounted = false;
55 };
56 }, [missionId]);
57
58 // Rendering
59 if (loading) return <div>Loading mission data...</div>;
60 if (error) return <div>Error: {error}</div>;
61
62 const { details, crew, telemetry } = missionData;
63
64 return (
65 <div className="mission-dashboard">
66 <h1>{details.name} - Control Panel</h1>
67
68 <section className="mission-details">
69 <h2>Mission Details</h2>
70 <p>Status: {details.status}</p>
71 <p>Objective: {details.objective}</p>
72 </section>
73
74 <section className="crew-info">
75 <h2>Crew</h2>
76 <ul>
77 {crew.map(member => (
78 <li key={member.id}>
79 {member.name} - {member.role}
80 </li>
81 ))}
82 </ul>
83 </section>
84
85 <section className="telemetry-data">
86 <h2>Telemetry</h2>
87 <p>Oxygen: {telemetry.oxygen}%</p>
88 <p>Fuel: {telemetry.fuel}%</p>
89 <p>Velocity: {telemetry.velocity} km/h</p>
90 </section>
91 </div>
92 );
93}In this example:
Promise.all() for parallel execution of multiple requestsmissionData stateTo improve readability and code maintenance, it's worth extracting the data fetching logic outside the component:
1// api.js - API Services
2const API_BASE_URL = 'https://api.spaceagency.com';
3
4export async function fetchAstronauts() {
5 const response = await fetch(`${API_BASE_URL}/astronauts`);
6 if (!response.ok) {
7 throw new Error(`HTTP Error: ${response.status}`);
8 }
9 return response.json();
10}
11
12export async function fetchMission(missionId) {
13 const response = await fetch(`${API_BASE_URL}/missions/${missionId}`);
14 if (!response.ok) {
15 throw new Error(`HTTP Error: ${response.status}`);
16 }
17 return response.json();
18}
19
20// Component with extracted API logic
21function AstronautsList() {
22 const [astronauts, setAstronauts] = useState([]);
23 const [loading, setLoading] = useState(true);
24 const [error, setError] = useState(null);
25
26 useEffect(() => {
27 let isMounted = true;
28
29 async function getAstronauts() {
30 try {
31 setLoading(true);
32 const data = await fetchAstronauts();
33
34 if (isMounted) {
35 setAstronauts(data);
36 setError(null);
37 }
38 } catch (err) {
39 if (isMounted) {
40 setError(`Failed to fetch data: ${err.message}`);
41 setAstronauts([]);
42 }
43 } finally {
44 if (isMounted) {
45 setLoading(false);
46 }
47 }
48 }
49
50 getAstronauts();
51
52 return () => {
53 isMounted = false;
54 };
55 }, []);
56
57 // Rendering
58 //
59}For repeatable data fetching operations, the best solution is to create a custom hook:
1// useFetch.js - Custom hook for data fetching
2function useFetch(url, options = {}) {
3 const [data, setData] = useState(null);
4 const [loading, setLoading] = useState(true);
5 const [error, setError] = useState(null);
6
7 useEffect(() => {
8 let isMounted = true;
9 const abortController = new AbortController();
10 const signal = abortController.signal;
11
12 async function fetchData() {
13 try {
14 setLoading(true);
15
16 const response = await fetch(url, {
17 ...options,
18 signal,
19 });
20
21 if (!response.ok) {
22 throw new Error(`HTTP Error: ${response.status}`);
23 }
24
25 const result = await response.json();
26
27 if (isMounted) {
28 setData(result);
29 setError(null);
30 }
31 } catch (err) {
32 if (err.name !== 'AbortError' && isMounted) {
33 setError(`Failed to fetch data: ${err.message}`);
34 setData(null);
35 }
36 } finally {
37 if (isMounted) {
38 setLoading(false);
39 }
40 }
41 }
42
43 fetchData();
44
45 return () => {
46 isMounted = false;
47 abortController.abort();
48 };
49 }, [url, JSON.stringify(options)]);
50
51 return { data, loading, error };
52}
53
54// Using the custom hook
55function PlanetInfo({ planetId }) {
56 const { data: planet, loading, error } = useFetch(
57 `https://api.spaceagency.com/planets/${planetId}`
58 );
59
60 if (loading) return <div>Loading...</div>;
61 if (error) return <div>Error: {error}</div>;
62 if (!planet) return <div>Planet not found</div>;
63
64 return (
65 <div className="planet-card">
66 <h2>{planet.name}</h2>
67 <p>Type: {planet.type}</p>
68 <p>Diameter: {planet.diameter} km</p>
69 <p>Distance from Sun: {planet.distanceFromSun} million km</p>
70 </div>
71 );
72}1// useDataFetching.js
2function useDataFetching(fetchFn, dependencies = [], options = {}) {
3 const {
4 initialData = null,
5 cacheKey = null,
6 maxRetries = 3,
7 retryDelay = 1000,
8 } = options;
9
10 const [data, setData] = useState(initialData);
11 const [loading, setLoading] = useState(true);
12 const [error, setError] = useState(null);
13
14 // Using useRef to track attempts
15 const retriesRef = useRef(0);
16
17 // Check cache on first render
18 useEffect(() => {
19 if (cacheKey) {
20 const cachedData = localStorage.getItem(cacheKey);
21 if (cachedData) {
22 try {
23 setData(JSON.parse(cachedData));
24 setLoading(false);
25 } catch (e) {
26 console.error('Error parsing cached data:', e);
27 }
28 }
29 }
30 }, [cacheKey]);
31
32 // Main data fetching logic
33 useEffect(() => {
34 let isMounted = true;
35 const abortController = new AbortController();
36
37 async function fetchData() {
38 try {
39 setLoading(true);
40
41 const result = await fetchFn(abortController.signal);
42
43 if (isMounted) {
44 setData(result);
45 setError(null);
46
47 // Save to cache if available
48 if (cacheKey) {
49 localStorage.setItem(cacheKey, JSON.stringify(result));
50 }
51
52 // Reset retry counter
53 retriesRef.current = 0;
54 }
55 } catch (err) {
56 if (!isMounted) return;
57
58 if (err.name === 'AbortError') {
59 console.log('Request cancelled');
60 return;
61 }
62
63 // Retry logic
64 if (retriesRef.current < maxRetries) {
65 console.log(`Attempt ${retriesRef.current + 1} of ${maxRetries} failed. Retrying in ${retryDelay}ms`);
66
67 setTimeout(() => {
68 if (isMounted) {
69 retriesRef.current += 1;
70 fetchData(); // Retry
71 }
72 }, retryDelay * Math.pow(2, retriesRef.current)); // Exponential backoff
73
74 return;
75 }
76
77 // After exhausting all attempts
78 setError(`Failed to fetch data: ${err.message}`);
79 setData(initialData);
80 } finally {
81 if (isMounted) {
82 setLoading(false);
83 }
84 }
85 }
86
87 fetchData();
88
89 return () => {
90 isMounted = false;
91 abortController.abort();
92 };
93 }, [...dependencies]);
94
95 // Function for manual data refresh
96 const refetch = useCallback(async () => {
97 setLoading(true);
98 retriesRef.current = 0;
99
100 try {
101 const abortController = new AbortController();
102 const result = await fetchFn(abortController.signal);
103 setData(result);
104 setError(null);
105
106 if (cacheKey) {
107 localStorage.setItem(cacheKey, JSON.stringify(result));
108 }
109 } catch (err) {
110 setError(`Failed to refresh data: ${err.message}`);
111 } finally {
112 setLoading(false);
113 }
114 }, [fetchFn, cacheKey]);
115
116 return { data, loading, error, refetch };
117}
118
119// Usage example
120function MarsMissions() {
121 // Function to fetch data
122 const fetchMissions = useCallback(async (signal) => {
123 const response = await fetch('https://api.spaceagency.com/mars/missions', { signal });
124 if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
125 return response.json();
126 }, []);
127
128 // Using the advanced hook
129 const {
130 data: missions,
131 loading,
132 error,
133 refetch
134 } = useDataFetching(fetchMissions, [], {
135 initialData: [],
136 cacheKey: 'mars-missions-cache',
137 maxRetries: 2
138 });
139
140 return (
141 <div className="mars-missions">
142 <div className="header">
143 <h2>Mars Missions</h2>
144 <button onClick={refetch} disabled={loading}>
145 {loading ? 'Refreshing...' : 'Refresh'}
146 </button>
147 </div>
148
149 {loading && missions.length === 0 && <div>Loading missions...</div>}
150 {error && <div className="error">Error: {error}</div>}
151
152 <ul className="missions-list">
153 {missions.map(mission => (
154 <li key={mission.id} className={`mission-item ${mission.status}`}>
155 <h3>{mission.name}</h3>
156 <p>Launch: {new Date(mission.launchDate).toLocaleDateString()}</p>
157 <p>Status: {mission.status}</p>
158 </li>
159 ))}
160 </ul>
161
162 {missions.length === 0 && !loading && !error && (
163 <p>No missions found.</p>
164 )}
165 </div>
166 );
167}In this advanced example:
useDataFetching hook with retry support, caching, and manual refresh functionalityBeyond basic error handling in components, it's worth implementing more advanced techniques:
Error boundaries allow elegant handling of unexpected errors:
1// ErrorBoundary.js
2class ErrorBoundary extends React.Component {
3 constructor(props) {
4 super(props);
5 this.state = { hasError: false, error: null, errorInfo: null };
6 }
7
8 static getDerivedStateFromError(error) {
9 // Update state so next render shows fallback UI
10 return { hasError: true };
11 }
12
13 componentDidCatch(error, errorInfo) {
14 // You can also log the error to a monitoring service
15 console.error('Caught error:', error, errorInfo);
16 this.setState({ error, errorInfo });
17
18 // Send error to reporting service
19 // logErrorToService(error, errorInfo);
20 }
21
22 render() {
23 if (this.state.hasError) {
24 // You can render any fallback UI
25 return (
26 <div className="error-boundary">
27 <h2>Something went wrong!</h2>
28 <p>An error occurred while rendering the component.</p>
29 <details>
30 <summary>Error details</summary>
31 <p>{this.state.error && this.state.error.toString()}</p>
32 <p>Components: {this.state.errorInfo && this.state.errorInfo.componentStack}</p>
33 </details>
34 <button onClick={() => this.setState({ hasError: false })}>
35 Try again
36 </button>
37 </div>
38 );
39 }
40
41 return this.props.children;
42 }
43}
44
45// Using error boundary for a component
46function App() {
47 return (
48 <div className="app">
49 <header>Mission Control Center</header>
50
51 <ErrorBoundary>
52 <MissionDashboard missionId="mars-2030" />
53 </ErrorBoundary>
54
55 <ErrorBoundary>
56 <AstronautsList />
57 </ErrorBoundary>
58
59 <footer>Space Agency © 2023</footer>
60 </div>
61 );
62}When an API is unstable, it's worth implementing automatic retry strategies:
1async function fetchWithRetry(url, options = {}, maxRetries = 3, delay = 1000) {
2 let retries = 0;
3
4 while (retries < maxRetries) {
5 try {
6 const response = await fetch(url, options);
7
8 if (!response.ok) {
9 throw new Error(`HTTP Error: ${response.status}`);
10 }
11
12 return await response.json();
13 } catch (err) {
14 retries++;
15
16 if (retries >= maxRetries) {
17 throw err; // All attempts failed, pass error forward
18 }
19
20 console.log(`Attempt ${retries} of ${maxRetries} failed. Retrying in ${delay}ms`);
21
22 // Wait before next attempt (exponential backoff)
23 await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, retries - 1)));
24 }
25 }
26}It's also worth handling internet connectivity issues:
1// Hook for monitoring connection status
2function useOnlineStatus() {
3 const [isOnline, setIsOnline] = useState(navigator.onLine);
4
5 useEffect(() => {
6 function handleOnline() {
7 setIsOnline(true);
8 }
9
10 function handleOffline() {
11 setIsOnline(false);
12 }
13
14 window.addEventListener('online', handleOnline);
15 window.addEventListener('offline', handleOffline);
16
17 return () => {
18 window.removeEventListener('online', handleOnline);
19 window.removeEventListener('offline', handleOffline);
20 };
21 }, []);
22
23 return isOnline;
24}
25
26// Usage in a component
27function DataFetchingComponent() {
28 const isOnline = useOnlineStatus();
29 const { data, loading, error, refetch } = useFetch('https://api.example.com/data');
30
31 if (!isOnline) {
32 return (
33 <div className="offline-message">
34 <h3>No internet connection</h3>
35 <p>Check your connection and try again.</p>
36 </div>
37 );
38 }
39
40 if (loading) return <div>Loading...</div>;
41 if (error) return <div>Error: {error}</div>;
42
43 return (
44 <div>
45 <h2>Data</h2>
46 {/* Rendering data */}
47 </div>
48 );
49}During application development, the real API may be unavailable or not ready. In such cases, it's worth simulating the API:
1// mockApi.js
2const DELAY = 1000; // Simulating network delay
3
4export function mockFetch(data, shouldFail = false, failureRate = 0) {
5 return new Promise((resolve, reject) => {
6 setTimeout(() => {
7 // Simulating random error
8 if (shouldFail || Math.random() < failureRate) {
9 reject(new Error('Simulated API error'));
10 return;
11 }
12
13 resolve(data);
14 }, DELAY);
15 });
16}
17
18// Sample data
19const MOCK_ASTRONAUTS = [
20 { id: 1, name: 'John Smith', role: 'Commander' },
21 { id: 2, name: 'Anna Johnson', role: 'Pilot' },
22 { id: 3, name: 'Peter Williams', role: 'Flight Engineer' }
23];
24
25export function getAstronauts() {
26 return mockFetch(MOCK_ASTRONAUTS, false, 0.2);
27}MSW is a library that simulates API requests at the browser level:
1// mockServiceWorker.js
2import { setupWorker, rest } from 'msw';
3
4// Simulation data
5const astronauts = [
6 { id: 1, name: 'John Smith', role: 'Commander' },
7 { id: 2, name: 'Anna Johnson', role: 'Pilot' },
8 { id: 3, name: 'Peter Williams', role: 'Flight Engineer' }
9];
10
11const missions = [
12 {
13 id: 'mars-2030',
14 name: 'Mars 2030',
15 status: 'Planned',
16 launchDate: '2030-05-15',
17 commander: 'John Smith'
18 },
19 {
20 id: 'iss-2023',
21 name: 'ISS Expedition 70',
22 status: 'In Progress',
23 launchDate: '2023-09-01',
24 commander: 'Anna Johnson'
25 }
26];
27
28// Defining handlers
29export const handlers = [
30 // Fetching all astronauts
31 rest.get('https://api.spaceagency.com/astronauts', (req, res, ctx) => {
32 return res(
33 ctx.delay(1000),
34 ctx.status(200),
35 ctx.json(astronauts)
36 );
37 }),
38
39 // Fetching astronaut by ID
40 rest.get('https://api.spaceagency.com/astronauts/:id', (req, res, ctx) => {
41 const { id } = req.params;
42 const astronaut = astronauts.find(a => a.id === parseInt(id));
43
44 if (!astronaut) {
45 return res(
46 ctx.delay(500),
47 ctx.status(404),
48 ctx.json({ error: 'Astronaut not found' })
49 );
50 }
51
52 return res(
53 ctx.delay(500),
54 ctx.status(200),
55 ctx.json(astronaut)
56 );
57 }),
58
59 // Fetching missions
60 rest.get('https://api.spaceagency.com/missions', (req, res, ctx) => {
61 return res(
62 ctx.delay(800),
63 ctx.status(200),
64 ctx.json(missions)
65 );
66 }),
67
68 // Fetching mission by ID
69 rest.get('https://api.spaceagency.com/missions/:id', (req, res, ctx) => {
70 const { id } = req.params;
71 const mission = missions.find(m => m.id === id);
72
73 if (!mission) {
74 return res(
75 ctx.delay(500),
76 ctx.status(404),
77 ctx.json({ error: 'Mission not found' })
78 );
79 }
80
81 return res(
82 ctx.delay(700),
83 ctx.status(200),
84 ctx.json(mission)
85 );
86 })
87];
88
89// Initialize service worker
90export const worker = setupWorker(...handlers);Then in your application's entry file (e.g.,
index.js):1// index.js
2import React from 'react';
3import { createRoot } from 'react-dom/client';
4import App from './App';
5
6// Conditionally initialize mock API only in development environment
7if (process.env.NODE_ENV === 'development') {
8 const { worker } = require('./mockServiceWorker');
9 worker.start();
10}
11
12const root = createRoot(document.getElementById('root'));
13root.render(<App />);Fetching data from APIs is a fundamental part of most React applications. The key to success is:
Remember that good data fetching handling significantly impacts user experience. Even if the API is slow, a well-designed interface with loading state information, animations, and error handling can make the application feel faster and more reliable.