JavaScript and React course Β· Module 5: useEffect and Custom Hooks
API Handling and Data Fetching
In this lesson6
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.
Basics of fetching data from an API
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.
Simple example of fetching data with useEffect
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:
- We use
useStateto store data, loading state, and errors - We use
useEffectwith an empty dependency array to fetch data only once after mounting - We use
async/awaitto handle asynchronous data fetching - We handle different states (loading, error, success) in the user interface
Advanced data fetching techniques
1. Fetching data with parameters
Often 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:
- We fetch data based on
missionId, which can change - We use an
isMountedflag to prevent state updates after unmount - We add a cleanup function that sets
isMounted = falseon unmount
2. Canceling fetch requests
The 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:
- We use
AbortControllerto create a signal that can abort a Fetch request - We pass the signal to the Fetch request options
- In the cleanup function, we call
abortController.abort()to cancel an ongoing request - We specifically handle the
AbortErrorthat is thrown when a request is cancelled
3. Handling multiple concurrent requests
Sometimes 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:
- We use
Promise.all()for parallel execution of multiple requests - We gather all data in a single
missionDatastate - We handle all data in one component
Best practices and patterns
1. Extracting data fetching logic
To 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}2. Creating custom hooks for data fetching
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}3. More advanced data fetching hook with caching and retry mechanism
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:
- We use a custom
useDataFetchinghook with retry support, caching, and manual refresh functionality - We implement exponential backoff for retries (each subsequent attempt waits longer)
- We use parameters to customize hook behavior for different use cases
- We handle both initial loading and manual data refresh
Error handling when fetching data
Beyond basic error handling in components, it's worth implementing more advanced techniques:
1. Error Boundary component
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}2. Retry strategies
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}3. Handling connectivity issues
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}API simulation during development
During application development, the real API may be unavailable or not ready. In such cases, it's worth simulating the API:
1. Simple simulation with delay
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}2. More sophisticated simulation with MSW (Mock Service Worker)
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 />);Summary
Fetching data from APIs is a fundamental part of most React applications. The key to success is:
- Code organization - separating API logic from components
- Custom hooks - creating abstractions for repeatable data fetching operations
- Handling all states - loading, success, error, offline
- Resource cleanup - canceling requests, preventing state updates after unmount
- Retry strategies - automatically retrying failed requests
- Testing - simulating APIs during development and tests
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.
Code for this lesson: App.jsx
1import React, { useState, useEffect } from 'react';
2
3// Custom hook for fetching data
4function useFetch(url, options = {}) {
5 const [data, setData] = useState(null);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState(null);
8
9 useEffect(() => {
10 const controller = new AbortController();
11
12 const fetchData = async () => {
13 setLoading(true);
14 setError(null);
15
16 try {
17 // Simulating network delay
18 await new Promise(resolve => setTimeout(resolve, 1500));
19
20 // Simulating data from the API
21 const mockData = getMockData(url);
22 setData(mockData);
23 } catch (err) {
24 if (err.name !== 'AbortError') {
25 setError(err.message);
26 }
27 } finally {
28 setLoading(false);
29 }
30 };
31
32 fetchData();
33
34 // Cleanup - canceling the request
35 return () => controller.abort();
36 }, [url]);
37
38 return { data, loading, error };
39}
40
41// Simulating an API response
42function getMockData(url) {
43 const endpoints = {
44 '/api/planets': [
45 { id: 1, name: 'Mars', distance: '225M km', status: 'Colonization' },
46 { id: 2, name: 'Jupiter', distance: '628M km', status: 'Exploration' },
47 { id: 3, name: 'Saturn', distance: '1.2B km', status: 'Observation' },
48 { id: 4, name: 'Uranus', distance: '2.7B km', status: 'Planning' },
49 ],
50 '/api/missions': [
51 { id: 1, name: 'Apollo 25', crew: 6, status: 'In progress' },
52 { id: 2, name: 'Artemis 12', crew: 4, status: 'Preparation' },
53 { id: 3, name: 'Europa Clipper', crew: 0, status: 'Automatic' },
54 ],
55 '/api/astronauts': [
56 { id: 1, name: 'Anna Kowalska', role: 'Captain', missions: 5 },
57 { id: 2, name: 'Jan Nowak', role: 'Engineer', missions: 3 },
58 { id: 3, name: 'Maria Wisniewska', role: 'Scientist', missions: 7 },
59 ],
60 };
61 return endpoints[url] || [];
62}
63
64// Component displaying planets
65function PlanetsList() {
66 const { data, loading, error } = useFetch('/api/planets');
67
68 if (loading) return <div className="loading">Loading planets...</div>;
69 if (error) return <div className="error">Error: {error}</div>;
70
71 return (
72 <div className="data-card">
73 <h2>Planets</h2>
74 <ul>
75 {data?.map(planet => (
76 <li key={planet.id}>
77 <strong>{planet.name}</strong>
78 <span className="detail">Distance: {planet.distance}</span>
79 <span className="status">{planet.status}</span>
80 </li>
81 ))}
82 </ul>
83 </div>
84 );
85}
86
87// Component displaying missions
88function MissionsList() {
89 const { data, loading, error } = useFetch('/api/missions');
90
91 if (loading) return <div className="loading">Loading missions...</div>;
92 if (error) return <div className="error">Error: {error}</div>;
93
94 return (
95 <div className="data-card">
96 <h2>Missions</h2>
97 <ul>
98 {data?.map(mission => (
99 <li key={mission.id}>
100 <strong>{mission.name}</strong>
101 <span className="detail">Crew: {mission.crew}</span>
102 <span className="status">{mission.status}</span>
103 </li>
104 ))}
105 </ul>
106 </div>
107 );
108}
109
110// Component displaying astronauts
111function AstronautsList() {
112 const { data, loading, error } = useFetch('/api/astronauts');
113
114 if (loading) return <div className="loading">Loading astronauts...</div>;
115 if (error) return <div className="error">Error: {error}</div>;
116
117 return (
118 <div className="data-card">
119 <h2>Astronauts</h2>
120 <ul>
121 {data?.map(astronaut => (
122 <li key={astronaut.id}>
123 <strong>{astronaut.name}</strong>
124 <span className="detail">Role: {astronaut.role}</span>
125 <span className="status">Missions: {astronaut.missions}</span>
126 </li>
127 ))}
128 </ul>
129 </div>
130 );
131}
132
133function App() {
134 const [refreshKey, setRefreshKey] = useState(0);
135
136 return (
137 <div className="app" key={refreshKey}>
138 <h1>Space Data Center</h1>
139 <button className="refresh-btn" onClick={() => setRefreshKey(k => k + 1)}>
140 Refresh all data
141 </button>
142 <div className="data-grid">
143 <PlanetsList />
144 <MissionsList />
145 <AstronautsList />
146 </div>
147 </div>
148 );
149}
150
151export default App;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the main difference between useEffect(() => {}, []) and componentDidMount?
2. What happens when we do NOT pass a dependency array to useEffect (no second argument)?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
States: [data, setData], [loading, setLoading], [error, setError]. useEffect: setLoading(true), fetch('/api/satellite'), setData(result), setLoading(false). Try/catch for errors. Display: loading ? 'Loading...' : data!
- Horizontal ordering
Arrange the correct useEffect syntax with a dependency array:
- Code editor
WebSocket Communication System
- Vertical ordering
Order the React component lifecycle phases:
- Code editor
Global Keyboard Shortcuts System