p3-insta485-clientside
Data Fetching in React with useEffect
This tutorial will explain with a comprehensive example how to fetch data in React with useEffect
while avoiding common problems.
In particular, we’ll explain why a dependency array and a cleanup function are required to avoid race conditions.
Prerequisites
This tutorial assumes some prerequisite knowledge.
- EECS 485 React tutorial explains React basics.
- Keeping Components Pure explains why React components avoid side-effects.
- Synchronizing with Effects explains how to add intentional side-effects.
TL;DR
Use an accurate dependency array every time you include useEffect
. Use the Fetch-and-ignore idiom (our term) when your React app makes a REST API request and changes its own state in response. Also, use custom hooks to extract common logic. The Fetch-and-ignore idiom avoids race conditions using these techniques. Read the rest of this tutorial to learn why.
// Simplified example of a component that fetches data in `useEffect` without a custom hook.
function Example({ url }) {
useEffect(() => {
let ignoreStaleRequests; // Boolean flag for ignoring requests
fetch(url)
.then((response) => {
return response.json();
})
.then((data) => {
if (ignoreStaleRequests) {
// If the flag has been set by the cleanup function, exit early.
return;
}
// Do stuff with the fetched data.
}
});
return () => {
// Cleanup function: set the boolean flag to true when the component unmounts.
ignoreStaleRequests = true;
};
}, [url]); // Dependency array
}
Introduction
Component rendering should be free of side-effects, that is, pure. Rendering should be nothing more than computing an output (JSX) from an input (props, state, and context). Components can perform side-effects in event handlers, but fetching API data requires the useEffect hook.
Data fetching is a side-effect because it reads or writes state outside of the component, for example by calling a REST API. Furthermore, after fetching data, you’ll likely want to update the component’s state and re-render the component. If a component changes its own state while rendering, its rendering is not pure. Side-effects are impossible to avoid, but we want to avoid them during rendering.
The React hook useEffect
implements a side-effect, safely avoiding the side-effect during rendering. However, useEffect
is complex, and it poses a serious risk of making your code harder to understand and less performant. For this reason, you might not need an effect, and you should consider carefully before you decide to use useEffect
.
This tutorial shows how to safely fetch data using useEffect
. It illustrates the best practices of a dependency array and a cleanup function, and explains why they are needed.
Example
To demonstrate data fetching, we’ll use a small example of an autocomplete widget. When you request "a"
from the mock API, you should get back words that start with "a"
, and when you request "aa"
, you should get back words that start with "aa"
. The final result should look something like this:
An App
component initially renders a List
that requests results from /api/?q=a
. Then App
will re-render and trigger its List
to re-render as well, this time set to request results from /api/?q=aa
. We’ll implement the List
component in different ways throughout the rest of this tutorial.
// App is the root component for this app.
export default function App() {
// App will render a List component with a specific URL provided from the App's state.
// It'll start out as /api/?q=a.
const [url, setUrl] = useState("/api/?q=a");
// After App renders initially, simulate a user typing a second "a," changing the autocomplete
// REST API request to /api/?q=aa. App will re-render and List will also re-render with the new
// url.
useEffect(() => {
setUrl("/api/?q=aa");
}, []);
return (
<div className="App">
<h1>Request to {url}</h1>
<List url={url} />
</div>
);
}
Mock fetch
During its lifetime, the App
component will render List
two times, sending two different requests with fetch
. In a perfect world, the first response would arrive first. In a real network, the two responses could arrive out of order.
We’ll replicate out-of-order responses by replacing fetch
with mockFetchSwap
. When mockFetchSwap
is called twice, it swaps the order of the responses.
Note: You don’t need to understand this code to understand Problem #1; just make sure to come back to it when you read about Problem #2.
// mockFetch will look up URLs and return hardcoded responses
const DATA = {
"/api/?q=a": JSON.stringify(["aardvark", "aardwolf", "abacus"]),
"/api/?q=aa": JSON.stringify(["aardvark", "aardwolf"])
};
// SWAP function will manually resolve every other request
let SWAP = null;
function mockFetch(url) {
// Return Promise that resolves to JSON Response
//
// Note: Promise handlers are always asynchronous, even when a Promise is
// immediately resolved. https://javascript.info/microtask-queue
console.assert(url in DATA);
const response = new Response(DATA[url]);
return Promise.resolve(response);
}
export default function mockFetchSwap(url) {
if (SWAP) {
// Resolve the pending promise from a previous call to this function by
// calling SWAP(). Then return the value for the current fetch.
SWAP();
SWAP = null;
return mockFetch(url);
} else {
// Return a Promise chain that resolves after SWAP() is called.
return new Promise((resolve) => {
SWAP = resolve;
}).then(() => {
return mockFetch(url);
});
}
}
In the next section, we’ll try implementing the List
component.
Problem #1: empty dependency array
For our first attempt, when List
sends a request, we’ll use useEffect
with an empty dependency array.
The App
component will first render a List
component with its url
prop set to "a"
. The List
will make a request for the query "a"
, and get back a pending Promise. Then the App
will re-render and trigger the List
to re-render as well with its url
prop set to "aa"
. We expect to see a list of words that start with “aa”. Let’s see what actually happens:
Note: Editing the code in the sandbox will trigger a re-render, which may affect the results. Don’t edit it until after you’ve observed the initial results.
The list of results is empty! Because the dependency array is empty, the effect only ran once, the first time the List
rendered to the screen. So, even though we changed the url
, the List
didn’t re-render and make a request to the new url
.
Pitfall: An empty dependency array can result in failure to re-render.
If there were no dependency array at all (distinct from an empty array), the effect would run every single time the component renders. The effect updates the List
’s state, and updating state triggers a re-render, so running this effect on every render would cause an infinite loop. The result would be a punishingly slow user experience.
Pitfall: A missing dependency array can result in infinite re-render.
Always use a dependency array with useEffect
unless you’re absolutely certain that you want the effect to run on every render. ESLint will tell you if the values in the dependency array are wrong. You can read about removing effect dependencies in the docs if you disagree with the linter.
In the next section, we’ll fix our missing output problem.
Problem #2: missing cleanup function
Now let’s fix the dependency array by adding url
to it, causing the effect to run both after the first render and again every time the List
renders with a different url
. Otherwise, we’ll leave the code the same.
Note: Editing the code in the sandbox will trigger a re-render, which may affect the results. Don’t edit it until after you’ve observed the initial results.
Now we’re getting results, but the results are wrong! The second request was for the query "aa"
, but one of the words in the results is “abacus,” which does not begin with “aa.”
The List
made a request for "a"
followed by a request for "aa"
, but our mock fetch reversed the order of the responses, simulating out-of-order network conditions.
-
The first response to arrive was for the second request
"aa"
. TheList
displays the results for"aa"
. -
The second response to arrive was for first request
"a"
. TheList
displays the results for"a"
.
We have a race condition: the behavior of our component depends on the order of network responses instead of the order of client interactions!
Race conditions are not unique to React. Any client-side dynamic pages app that synchronizes with a REST API is vulnerable to race conditions.
Solution
The solution to this race condition is a cleanup function. The cleanup function will discard results from stale requests.
A cleanup function is called every time the component unmounts or re-renders. In the cleanup function, we can add logic to ignore the response to a stale request.
Our app now correctly displays only words starting with “aa”, handling out-of-order responses using the Fetch-and-ignore idiom.
- Effect #1 starts a request for “a”.
- List re-renders, setting ignore = true for effect #1 and starting a request for “aa” in effect #2.
- The “aa” response arrives first (ignore = false), so List displays it.
- The “a” response arrives later (ignore = true), so List discards it.
Pitfall: Failure to implement the Fetch-and-ignore idiom may result in a race condition.
An alternative to a Boolean variable is AbortController
, which cancels a request instead of ignoring the results.
Problem #3: duplicate code
Every time a component uses fetch
, it must correctly use a dependency list and correctly implement the fetch-and-ignore idiom. The result is duplicate code.
Avoid duplicate code by writing a custom hook for data fetching so that you don’t need to repeat all of this boilerplate every time you fetch data.
Custom hooks also help with advanced features like data caching and future-proofing. In the future, React’s Suspense will handle data fetching without useEffect, making refactoring easier.
The React Docs have an example of using a custom hook for data fetching!
Summary
Data fetching is an elaborate problem. The current React mechanism for data fetching is the useEffect
hook. The useEffect
hook has several fatal pitfalls, including missing results, infinite re-render, and race conditions.
Avoid the pitfalls of useEffect
by including a dependency array and a cleanup function when you fetch data with useEffect
.
Further reading
We found these documents helpful during the writing of this tutorial.
- You Might Not Need an Effect in the React docs
- Lifecycle of Reactive Effects in the React docs
- Separating Events from Effects in the React docs
- Removing Effect Dependencies in the React docs
- Reusing Logic with Custom Hooks in the React docs
- A Complete Guide to useEffect by Dan Abramov
Acknowledgments
Original document written by Noah Weingarden nwein@umich.edu and Andrew DeOrio awdeorio@umich.edu.
This document is licensed under a Creative Commons Attribution-NonCommercial 4.0 License. You’re free to copy and share this document, but not to sell it. You may not share source code provided with this document.