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
A React component can often perform side-effects in an event handler. A conspicuous exception is a component that immediately displays data fetched from an API. In this case, we can use the React hook useEffect.
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).
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. When url
changed, the List
failed to re-run its effect, which would have otherwise made 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.
This is better, since 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"
. 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.
Finally, our app show correct results! The results are only the words that begin with "aa"
.
As before, the List
made a request for "a"
followed by a request for "aa"
. Our mock fetch reversed the order of the responses, simulating out-of-order network conditions.
- A request for
"a"
starts in effect #1. List
re-renders, setting theignore
variable in effect #1 totrue
and starting a request for"aa"
in effect #2.- The first response to arrive was for the second request
"aa"
in effect #2. Theignore
variable isfalse
. TheList
displays the results for"aa"
. - The second response to arrive was for the first request
"a"
in effect #1. Theignore
variable istrue
. TheList
discards the results for"a"
.
We like to call this the Fetch-and-ignore idiom.
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.
Another motivation for a custom hook is if you want to augment the hook with more complicated operations like data caching.
A third motivation for a custom hook is in the future, data fetching on the client in React won’t even involve useEffect
: it will involve a feature called Suspense. Using a custom hook will make it easier to refactor code that previously fetched data using this method so that it uses the (not yet production-ready) built-in React method instead.
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.