React Intermediate: Forms, Hooks and State
Fetching Data
In this lesson you will
- Fetch data in an effect with loading and error states
- Cancel stale requests to avoid race conditions
- Describe what data-fetching libraries add
Introduction
Most apps load data from a server. Doing it well means handling the time the data is on its way, the cases where it fails, and the case where the answer arrives too late. These are the details that separate a demo from a real app.
The three states
Every request is in one of three situations, and your UI must handle all of them:
- Loading: waiting for the response
- Error: it failed
- Success: you have data (which may be empty)
A reliable fetch hook
React (JSX)
import { useState, useEffect } from "react";
function useFetch(url) {
const [state, setState] = useState({ status: "loading", data: null, error: null });
useEffect(() => {
const controller = new AbortController();
setState({ status: "loading", data: null, error: null });
fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error("Request failed: " + res.status);
return res.json();
})
.then((data) => setState({ status: "success", data, error: null }))
.catch((error) => {
if (error.name !== "AbortError") {
setState({ status: "error", data: null, error });
}
});
return () => controller.abort();
}, [url]);
return state;
}
function UserList() {
const { status, data, error } = useFetch("/api/users");
if (status === "loading") return <p>Loading...</p>;
if (status === "error") return <p role="alert">Something went wrong: {error.message}</p>;
if (data.length === 0) return <p>No users yet.</p>;
return (
<ul>
{data.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}Points to notice:
fetchdoes not reject for HTTP errors like 404 or 500. You must checkres.okyourself.- The
AbortControllercancels the request in the clean-up function. - The
urlis in the dependency array, so a new URL triggers a new request.
Quick quiz
Why do we abort the request in the effect's clean-up function?
Race conditions
Imagine a search box. You type "re", then "react". The request for "re" is slower and arrives after the one for "react". Without cancellation, the screen shows results for "re" while the box says "react". Abort controllers, or an ignore flag in the effect, prevent this.
Good habits
- Show loading indicators, but avoid flashing them for very fast responses.
- Give errors a retry button.
- Debounce typing, so you do not send a request on every keystroke.
- Never put secrets, such as API keys, in front-end code.
- Handle empty results with a friendly message.
Data-fetching libraries
Writing all this by hand gets repetitive. Libraries such as TanStack Query and SWR add caching, request de-duplication, automatic refetching, retries and background updates. Frameworks such as Next.js also fetch data on the server before the page reaches the browser, which is faster and good for search engines. Learn the basics first, as above, so you understand what the libraries do for you.
Mutations
Sending data (POST, PUT, DELETE) has its own states: pending, success and error. Disable the button while pending, show errors, and update or refetch the displayed data afterwards. Some apps use optimistic updates, which show the change instantly and roll back if the server rejects it.
Try it yourself
Use the useFetch hook with a free public JSON API. Add a "Retry" button on the error state, and test the failure case by using a wrong URL.
Summary
- Handle loading, error and success states, and check
res.oksince fetch does not reject on HTTP errors. - Abort stale requests in the effect clean-up to avoid race conditions.
- Libraries such as TanStack Query add caching and refetching, so learn the basics first.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading