Fetch in an Effect, Store the Result in State
Data Fetching
The basic shape: an effect kicks off a fetch, a setter stores the result, the render shows loading / data / error.
What you'll learn
- Fetch data inside an effect
- Handle loading and error states
- Re-fetch when inputs change
For most apps, the typical data-loading pattern is an effect that starts a fetch and stores the result in state. Render shows one of three states: loading, error, or data.
The Shape
function UserCard({ id }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(`/api/users/${id}`)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => setUser(data))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, [id]);
if (loading) return <p>Loading…</p>;
if (error) return <p>Oops: {error.message}</p>;
return <p>{user.name}</p>;
} That’s it for the basic shape. The next lessons add two refinements: avoiding stale results and aborting in-flight requests.
Three States Pattern
| State | Show |
|---|---|
| Loading | A spinner, skeleton, or “Loading…” |
| Error | The error message, maybe a retry button |
| Data | The thing |
| (Empty) | “No results” if you’re showing a list |
A common refactor is to compress these into a single state object:
const [state, setState] = useState({ status: "loading" });
// {status: "loading"} | {status: "error", err} | {status: "data", data} A useReducer works well for this (chapter 5).
With async/await
The .then chain can be flattened with async/await inside the
effect — but you can’t make the effect callback itself async
(React requires it returns a cleanup or undefined). Define an inner
async function:
useEffect(() => {
async function load() {
try {
setLoading(true);
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setUser(await res.json());
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
load();
}, [id]); Re-Fetching On Change
Putting id in deps makes the effect re-run whenever id changes.
That’s exactly what you want for “show the user whose id is in the
URL”.
Real Apps Use A Library
For most real apps, raw fetch in useEffect is replaced by
React Query (TanStack Query) or SWR. They give you caching,
deduplication, retries, and revalidation — for free.
Why we still teach the raw version:
- It’s the mental model — libraries are productivity over the same primitives
- You’ll see it in older code and small projects
- Sometimes you really do need control over the request
Up Next
If the user changes id while a fetch is in flight, you can end up
showing the wrong user. That’s a race condition.