Fetching data
A component that shows data from a server doesn't have that data the moment it first renders. It has to ask for it, wait, and then show something once the answer arrives, or if the request fails along the way. This chapter covers the pattern React apps use for that: fetch inside an effect, and hold the result, a loading state, and an error in useState.
Here's a Profile component that fetches a user by URL. Rendering happens first, and reaching out to the server right after is exactly the moment useEffect exists for:
function Profile({ url }) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
let active = true
setData(null)
setError(null)
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`Request failed: ${r.status}`)
return r.json()
})
.then(d => { if (active) setData(d) })
.catch(e => { if (active) setError(e) })
return () => { active = false }
}, [url])
if (error) return <p>Failed to load</p>
if (!data) return <p>Loading...</p>
return <h1>{data.name}</h1>
}If fetch, promises, or .then are still unfamiliar, the JavaScript handbook covers them from the ground up in Async JavaScript; everything here builds directly on that.
useEffect is the hook that runs code after React has rendered a component, rather than during the render itself. You give it a function to run and a dependency array, the list of values that should make it run again when they change, here only url. Profile starts with two pieces of state: data for the result and error for anything that went wrong. At the top of the effect, setData(null) and setError(null) clear out whatever the previous run left behind. That matters for two reasons: without it, a url change would keep showing the old profile instead of "Loading...", since data still holds the last successful result, and a request that succeeds after an earlier one failed would never clear the old error, since nothing ever sets error back to null. Then fetch returns a promise for the response. fetch only rejects that promise on a network failure, so a 404 or 500 still resolves normally as a response object, which is why the first .then checks r.ok and throws before the response ever reaches .json(). Skip that check and a failed request would flow straight into data as if it had succeeded. Once the check passes, .json() reads the body and the result lands in data through setData. If any of this rejects, .catch puts the problem in error instead. Down in the JSX, error and data are checked like any other state: an error renders a message, no data yet renders a loading message, and data renders the real UI.
There's no separate loading variable here. As long as data and error are both still null, the component is between the request going out and something coming back, so if (!data) return <p>Loading...</p> covers that gap on its own. Some components do add a dedicated isLoading boolean for finer control, but deriving loading from state you already have is often simpler, and it's one fewer value to keep in sync.
The active variable inside the effect is a cleanup flag. useEffect can return a function, and React calls that function right before the effect runs again and when the component unmounts. Here, the returned function sets active to false, and both .then and .catch check it before calling setData or setError. Without that check, a request still in flight when url changes, or when the component leaves the screen entirely, would eventually resolve and try to update state that no longer belongs to what's showing. The flag turns a late response into a no-op instead of a bug.
Fetching by hand like this is worth understanding, because it's the pattern almost everything else builds on. In a real app you'll usually reach for a data-fetching library like React Query, or a framework's built-in data loading (Next.js and similar frameworks handle this at the routing layer), rather than write useEffect and a few pieces of state for every request. Those tools handle caching, retries, and the timing issues below for you. The pattern in this chapter is what they're built on.
useEffect, keep data and error in state, and check them in your JSX to decide what to show. Loading is the moment when both are still empty. The active flag stops a late response from setting state on a component that isn't showing that data anymore. Next up: Refs and the DOM, the escape hatch for the jobs that need the DOM node itself.

