Fetching API data in React means sending a request from a component and putting the response into state. You can fetch data in React with the browser's fetch function and a useEffect call, with no extra package. This guide builds that pattern step by step, with a loading state and cleanup that stops stale responses from winning.
Run the request in an Effect
A fetch starts because the component appeared, so it belongs in an Effect rather than during render. The Effect below runs once, reads a JSON endpoint, and stores the result in the posts state.
import { useEffect, useState } from "react";
export default function Posts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch("/api/posts")
.then((res) => res.json())
.then((data) => setPosts(data));
}, []);
}After the first render, React runs the Effect. The fetch promise resolves with a Response, the next call parses it as JSON, and the final callback saves the array into posts.
The empty dependency array means the request runs once instead of on every render. A changing endpoint would belong in that array, but a static URL keeps it empty.
Guard against stale responses
The plain version has a bug. If the component unmounts before the response arrives, the callback still calls setPosts on a component that is gone.
If the component later requests different data, two responses can race and the older one can win. An ignore flag fixes both problems.
useEffect(() => {
let ignore = false;
fetch("/api/posts")
.then((res) => res.json())
.then((data) => {
if (!ignore) setPosts(data);
});
return () => {
ignore = true;
};
}, []);The cleanup runs before the Effect re-runs and when the component unmounts. Setting ignore to true tells an old response to drop its data, so only the newest request can update state.
This is cheap insurance and costs nothing in production. The same stale response problem is covered in how to fetch data with useEffect without creating race conditions.
Check response.ok before reading the body
fetch rejects only when the network fails, not when the server returns an error status. A 404 or 500 still resolves into a Response, so check the status yourself before calling json.
fetch("/api/posts").then((response) => {
if (!response.ok) {
throw new Error(`Request failed with ${response.status}`);
}
return response.json();
});response.ok is true for status codes in the 200 range. Throwing here pushes the failure into the promise chain, where an error state can catch it instead of silently rendering an empty list.
Treating a non-2xx response as an error keeps a broken request from looking like missing data. If the server sends a JSON error body, read it in the catch block and show that message instead of a generic failure.
Show a loading state
A component that fetches data should tell the user what is happening. Silence makes a failure look like a missing feature. Add a status state next to posts, set it to "loading", "success", or "error" at each step of the fetch, and render a message for each state instead of leaving the screen blank.
const [status, setStatus] = useState("loading");
if (status === "loading") return <p role="status">Loading posts...</p>;
if (status === "error") return <p role="alert">Could not load posts.</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);Set status to loading before the request, success after the data arrives, and error when the request fails. Keep status as a single string rather than separate boolean flags, so the states can never contradict each other.
The role attributes announce these changes to screen readers. A full breakdown is in how to handle loading, error, empty, and success states.
When to use a data library instead
Fetching inside an Effect is fine for a small client-only app, but it has limits. Effects do not run on the server, so the initial HTML has no data. There is no caching, so navigating back re-fetches.
There is no deduplication, so two components requesting the same URL each run their own request. The React documentation recommends a framework's built-in data fetching or a client-side cache such as TanStack Query, SWR, or React Router for anything beyond simple cases.
You do not have to rewrite your component to benefit from this later. Moving the fetch into a custom Hook or setting up TanStack Query in React keeps the same UI while adding caching and request deduplication underneath. The visible screen stays the same, while repeated visits and shared requests become faster.
Rune AI
Key Insights
- Run the request in useEffect, not during render.
- Check response.ok before reading JSON.
- Store the parsed data in state.
- Use an ignore flag in cleanup to drop stale responses.
- Show a loading state while the request runs.
Frequently Asked Questions
Should I fetch data in an Effect?
Why does fetch not throw on a 404?
Do I need to cancel the request?
Conclusion
Fetch API data with fetch inside useEffect, store the parsed JSON in state, and guard against stale responses with an ignore flag in cleanup. For larger apps, move to a data library that adds caching and deduplication.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.