React Fetch Example: How to Use the Fetch API to Update Component State
To fetch data in React, call the browser's native fetch inside a useEffect hook and update state via useState, which triggers a re-render displaying the retrieved data.
The facebook/react repository demonstrates this pattern through core hooks defined in packages/react/src/ReactHooks.js and practical implementations in fixtures like fixtures/dom/src/tags.js. This approach leverages React’s declarative state management to synchronize your UI with external data sources.
Core Hooks for Data Fetching
React does not ship with a built-in HTTP client, so state management relies on two primitives exported from packages/react/src/ReactHooks.js:
useState– Creates reactive variables that persist across renders. When you call the setter (e.g.,setPosts), React schedules a re-render.useEffect– Registers side-effects that run after the component mounts. Passing an empty dependency array[]ensures the effect runs only once, making it ideal for initiating a single network request.
According to the React source code, useState<S> and useEffect are defined as exported functions in ReactHooks.js, providing the foundation for all data-fetching patterns in functional components.
React Fetch Example Implementation
Below is a complete, production-ready component that retrieves data from an external API and updates its local state. This example demonstrates proper handling of the asynchronous fetch lifecycle, including cancellation support.
import { useState, useEffect } from 'react';
export default function PostList() {
const [posts, setPosts] = useState([]); // holds fetched data
const [loading, setLoading] = useState(true); // UI loading flag
const [error, setError] = useState(null); // error handling
useEffect(() => {
const controller = new AbortController(); // enables cancellation
const signal = controller.signal;
(async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setPosts(data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
})();
// Cleanup runs when the component unmounts
return () => controller.abort();
}, []); // [] → run once on mount
if (loading) return <p>Loading…</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Key implementation details:
- Async IIFE inside
useEffect: The effect callback cannot beasyncdirectly, so an immediately invoked function expression wraps theawait fetchlogic. - AbortController: Passed as a signal to
fetch, this prevents state updates after the component unmounts, avoiding memory leaks. - Empty dependency array: The
[]argument guarantees the network request fires only once after the initial render.
Handling Loading, Error, and Cleanup States
A robust react fetch example must account for three UI states: pending, success, and failure. The pattern above uses separate state variables for each concern:
- Loading state: Initialized as
true, flipped tofalsein thefinallyblock so the UI can render a skeleton or spinner during the request. - Error state: Catches network failures or non-OK HTTP statuses, storing the message for user feedback while filtering out intentional aborts.
- Cleanup: The
useEffectreturn function invokescontroller.abort(). If the component unmounts before the promise resolves, the browser cancels the request and the catch block ignores the resultingAbortError.
This structure aligns with the fetch usage shown in fixtures/flight/src/App.js, where async data retrieval integrates directly into the render flow.
Reference Implementations in the React Repository
The React codebase provides concrete reference points for these patterns:
fixtures/dom/src/tags.js: Contains a real-world fetch call to the GitHub API that retrieves repository tags and parses JSON. It demonstrates the low-level Promise handling useful when working outside of React state.fixtures/flight/src/App.js: Shows a server-component example whereawait fetch(...)is used directly in the component body, illustrating how modern React can suspend rendering until data arrives without manually managinguseEffecton the server.
Both files complement the core hook definitions in ReactHooks.js, showing how the same Fetch API integrates with client-side effects and server-side async components.
Summary
- Use
useState(defined inpackages/react/src/ReactHooks.js) to store data, loading flags, and error messages. - Initiate
fetchinsideuseEffectwith an empty dependency array to run the request once on mount. - Update state via the setter function (e.g.,
setPosts) to trigger a re-render displaying the fetched data. - Implement
AbortControllercleanup to prevent state updates on unmounted components.
Frequently Asked Questions
Why can't I make the useEffect callback async directly?
React expects the useEffect callback to return either nothing or a cleanup function. If you declare the callback as async, it implicitly returns a Promise, which React cannot treat as a cleanup function. Instead, define an async function inside the effect (like an IIFE) and invoke it immediately.
Is AbortController necessary for every fetch in React?
While not strictly required, AbortController is essential for preventing memory leaks and race conditions. If a component unmounts while a request is pending, updating state on an unmounted component triggers React warnings. The abort signal cancels the fetch and allows you to ignore the resulting error.
Can I use this pattern in React Server Components?
Yes, but with a different syntax. As shown in fixtures/flight/src/App.js, server components can await fetch(...) directly without wrapping it in useEffect because the component executes on the server where side-effects run synchronously during the request lifecycle.
What are alternatives to the native Fetch API in React?
While the browser's fetch is standard, you may use libraries like axios or SWR for automatic caching, deduplication, and background revalidation. However, the underlying state update mechanism—calling a useState setter with the retrieved payload—remains identical regardless of the HTTP client chosen.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →