Fetching Data in React
HTTP requests in React are typically handled with the Fetch API and useEffect hook for side effects.
Basic Data Fetching
Here’s a complete example showing loading states, error handling, and data management:
import { useState, useEffect } from 'react';
import Places from './Places.jsx';
import Error from './Error.jsx';
export default function AvailablePlaces({ onSelectPlace }) {
const [isFetching, setIsFetching] = useState(false);
const [availablePlaces, setAvailablePlaces] = useState([]);
const [error, setError] = useState();
useEffect(() => {
async function fetchPlaces() {
setIsFetching(true);
try {
const response = await fetch('http://localhost:3000/places');
const resData = await response.json();
if (!response.ok) {
throw new Error('Failed to fetch places');
}
setAvailablePlaces(resData.places);
} catch (error) {
setError({
message:
error.message || 'Could not fetch places, please try again later.',
});
}
setIsFetching(false);
}
fetchPlaces();
}, []);
if (error) {
return <Error title="An error occurred!" message={error.message} />;
}
return (
<Places
title="Available Places"
places={availablePlaces}
isLoading={isFetching}
loadingText="Fetching place data..."
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
Always use useEffect for HTTP requests to avoid infinite re-render loops.
Using async/await
The async/await syntax makes asynchronous code more readable:
const response = await fetch('http://localhost:3000/places');
const data = await response.json();
if (!response.ok) {
throw new Error('Failed to fetch data');
}
Managing Loading States
Always provide feedback while data is loading:
function DataComponent() {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
setIsLoading(true);
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}
fetchData();
}, []);
if (isLoading) {
return <p>Loading...</p>;
}
return <div>{/* Render data */}</div>;
}
Use the finally block to ensure loading state is always reset, even if an error occurs.
Error Handling
Handle both network errors and HTTP errors:
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch('/api/data');
// Check if the response was successful
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data = await response.json();
setData(data);
} catch (error) {
// Catches both network errors and thrown errors
setError({
message: error.message || 'Something went wrong!'
});
}
}
fetchData();
}, []);
if (error) {
return (
<div className="error">
<h2>An error occurred!</h2>
<p>{error.message}</p>
</div>
);
}
Sending Data (POST Requests)
Send data to your backend with POST requests:
async function addPlace(place) {
try {
const response = await fetch('http://localhost:3000/user-places', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ placeId: place.id })
});
if (!response.ok) {
throw new Error('Failed to add place');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error adding place:', error);
throw error;
}
}
Optimistic Updates
Update the UI immediately and handle errors if the request fails:
function handleAddPlace(place) {
// Update UI immediately (optimistic update)
setUserPlaces(prevPlaces => {
if (!prevPlaces) {
prevPlaces = [];
}
if (prevPlaces.some(p => p.id === place.id)) {
return prevPlaces;
}
return [place, ...prevPlaces];
});
// Send request in the background
fetch('http://localhost:3000/user-places', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ placeId: place.id })
}).catch(error => {
// Revert on error
setUserPlaces(prevPlaces =>
prevPlaces.filter(p => p.id !== place.id)
);
setError({
message: error.message || 'Failed to add place.'
});
});
}
With optimistic updates, always handle errors and revert changes if the request fails.
DELETE Requests
Remove data from your backend:
async function handleRemovePlace(placeId) {
setUserPlaces(prevPlaces =>
prevPlaces.filter(place => place.id !== placeId)
);
try {
const response = await fetch(
`http://localhost:3000/user-places/${placeId}`,
{ method: 'DELETE' }
);
if (!response.ok) {
throw new Error('Failed to delete place');
}
} catch (error) {
// Restore the place on error
setUserPlaces(prevPlaces => [...prevPlaces, deletedPlace]);
setError({ message: 'Failed to delete place.' });
}
}
Create reusable HTTP functions:
export async function fetchAvailablePlaces() {
const response = await fetch('http://localhost:3000/places');
const resData = await response.json();
if (!response.ok) {
throw new Error('Failed to fetch places');
}
return resData.places;
}
export async function updateUserPlaces(places) {
const response = await fetch('http://localhost:3000/user-places', {
method: 'PUT',
body: JSON.stringify({ places }),
headers: {
'Content-Type': 'application/json'
}
});
const resData = await response.json();
if (!response.ok) {
throw new Error('Failed to update user data.');
}
return resData.message;
}
Custom useFetch Hook
Create a reusable hook for data fetching:
import { useEffect, useState } from 'react';
export function useFetch(fetchFn, initialValue) {
const [isFetching, setIsFetching] = useState();
const [error, setError] = useState();
const [fetchedData, setFetchedData] = useState(initialValue);
useEffect(() => {
async function fetchData() {
setIsFetching(true);
try {
const data = await fetchFn();
setFetchedData(data);
} catch (error) {
setError({ message: error.message || 'Failed to fetch data.' });
}
setIsFetching(false);
}
fetchData();
}, [fetchFn]);
return {
isFetching,
fetchedData,
error
}
}
Using the Hook
import { useFetch } from './hooks/useFetch';
import { fetchAvailablePlaces } from './http';
function AvailablePlaces() {
const {
isFetching,
fetchedData: availablePlaces,
error
} = useFetch(fetchAvailablePlaces, []);
if (error) {
return <Error title="Error" message={error.message} />;
}
return (
<Places
places={availablePlaces}
isLoading={isFetching}
loadingText="Loading places..."
/>
);
}
Best Practices
Network requests can fail. Always wrap fetch calls in try/catch blocks and provide user feedback.
Keep users informed while data is loading to prevent confusion.
The fetch API doesn’t reject on HTTP errors (like 404). Always check response.ok.
Include all dependencies in the dependency array to avoid stale closures.
Cancel ongoing requests when components unmount to prevent memory leaks.
Source Code: Section 15 - HTTP Requests