Data Fetching & API Integration
- React
- REST API
How a frontend loads data from an API and handles the three states every request has — loading, success, and error — while keeping the UI in sync.
Prerequisites
- Hooks and REST API basics (see Related Topics)
What you'll understand
- How a frontend requests data from an API
- The three states every request must handle
- Why fetched data is treated as server state
Explanation
Data fetching is how your frontend gets information from a backend API and shows it to the user. In the browser you make an HTTP request (commonly with the fetch API), receive a response, and render it. Because a request takes time and can fail, fetching is asynchronous, and the UI has to account for that rather than assuming the data is simply there.
Every request has three states, and a robust integration handles all of them: loading, while the request is in flight; success, when data arrives and is displayed; and error, when the request fails and the user needs to be told. Skipping the loading or error state is the most common source of janky, confusing interfaces. Checking the response status and catching failures (mirroring the server error handling from earlier modules) is part of the job.
Fetched data is server state: a local copy of something the server owns (see Related Topics). It can become stale, may need refetching, and belongs alongside its loading and error status, not mixed into ordinary client state. Simple cases can live in a component effect or a custom hook, but as an app grows, a dedicated data-fetching library that handles caching, refetching, and deduplication usually pays off.
Examples
This custom hook fetches data and exposes all three states so the UI can react to each:
function useUser(id: number) {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${id}`)
.then((res) => {
if (!res.ok) throw new Error('Request failed');
return res.json();
})
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [id]);
return { user, error, loading };
}Common mistakes
- Rendering as if data is always present, ignoring the loading and error states.
- Not checking the response status before using the body.
- Storing server data as plain client state with no notion of staleness.
- Fetching in a way that races or refetches on every render due to a bad dependency array.
Best practices
- Always handle loading, success, and error explicitly.
- Check the response status and handle failures (see Related Topics).
- Encapsulate fetching in a custom hook to reuse and simplify components.
- Use a data-fetching library for caching and refetching as needs grow.
Further reading
- React, Synchronizing with Effects — https://react.dev/learn/synchronizing-with-effects
- MDN, Using the Fetch API — https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch