Data Fetching
Overview
The vast majority of React components need to fetch dynamic data from a backend REST API or GraphQL server. Because making an HTTP request takes time (it is asynchronous) and interacts with the outside world, it is an archetypal Side Effect that must be placed inside a useEffect.
Building a robust Data Fetching component requires managing three distinct states: 1. Loading State: A boolean indicating if the request is currently in flight. 2. Error State: A string holding any errors if the server crashes or the network fails. 3. Data State: The actual JSON response from the server.
While fetching in useEffect is the foundational way to learn React, modern enterprise applications rarely write this boilerplate by hand. They use specialized data-fetching libraries like React Query (TanStack Query) or SWR, which automatically handle caching, retries, and background synchronization.
Syntax
function UserProfile() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// We must define the async function inside the effect
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/user');
if (!response.ok) throw new Error('Network failed');
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false); // Always stops loading, regardless of success/fail
}
};
fetchData(); // Execute it
}, []); // Run exactly once on mount
// ... render loading, error, or user
}Common Pitfalls
- Making useEffect async directly: You CANNOT write
useEffect(async () => { ... }). React expects the effect callback to return either nothing, or a synchronous Cleanup Function. An async function returns a Promise. To useasync/await, you must declare the async function inside the effect, and then immediately call it.
Interview Questions
The function passed to useEffect is only allowed to return a cleanup function (or nothing). Async functions inherently return a Promise. If React receives a Promise instead of a cleanup function, it breaks the cleanup mechanism. The workaround is defining the async function inside the effect body and invoking it.
A race condition occurs if a user triggers two fetches rapidly (e.g., clicking Profile 1, then Profile 2). If the server is slow, the response for Profile 1 might arrive after Profile 2, causing the UI to display the wrong data. You prevent this using a boolean flag (let isMounted = true) in the effect, and setting it to false in the cleanup function. You only call setState if isMounted is still true.
Real-World Example
Race Condition Prevention (AbortController): In an autocomplete search bar, the user might type 'a', 'p', 'p' very quickly. Without an AbortController in the cleanup function, you would fire 3 simultaneous network requests, wasting bandwidth and risking race conditions where the 'a' results overwrite the 'app' results.
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
// 1. Create a native browser AbortController
const controller = new AbortController();
async function search() {
try {
// 2. Attach the signal to the fetch request
const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal });
const data = await res.json();
setResults(data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted - User typed a new letter too fast!');
}
}
}
if (query) search();
// 3. Cleanup: If the query changes before the fetch finishes, abort the old network request entirely!
return () => controller.abort();
}, [query]);
return <ul>...</ul>;
}Check Your Knowledge
Test your understanding of Data Fetching with these quick questions.