React useEffect Prevent Multiple Renders
Intro
One of the most common issues with a useEffect hook is that it will repeatedly re-render and fire a function more than once, which is particularly undesirable during async operations. Here is the tried and true method for preventing multiple renders with useEffect.
Boilerplate
useEffect(() => {
const someAPICall = async () => {
console.log("🚀 calling api");
// your async operations go here
};
// request tracker
let loading = false;
try {
if (!loading) {
someAPICall();
}
} catch (error) {
console.error(error);
} finally {
// cleanup => request tracker
return () => (loading = true);
}
}, []);
Explanation
The key to this method is the request tracker variable loading
. This is a variable that is set to false by default, and then set to true in the cleanup function. The cleanup function is called when the component re-renders.
This means that the next time useEffect is called, the request tracker will be true, and the useEffect will not fire. This variable is scoped to this useEffect, so you can use this same pattern in multiple useEffects without worrying about them interfering with each other.
Chaining
This method is particularly useful for chaining useEffects that need to happen in a particular order. For example, if you need to fetch data from an API, and then use that data to fetch more data from another API, you can use this method to ensure that the second useEffect doesn't fire until the first one is complete by having the first one set a piece of state that the second is watching, like so:
const [idsFetched, setIdsFetched] = useState(false);
const [accountsFetched, setAccountsFetched] = useState(false);
// 1: fetch id array
useEffect(() => {
const fetchIds = async () => {
// fetch and store ids...
// if successful
setIdsFetched(true);
};
// request tracker
let loading = false;
try {
if (!loading && !idsFetched) {
fetchIds();
}
} catch (error) {
console.error(error);
} finally {
// cleanup => request tracker
return () => (loading = true);
}
}, [idsFetched, setIdsFetched]);
// 2: fetch accounts using id's
useEffect(() => {
const fetchAccounts = async () => {
// fetch and store accounts...
// if successful
setAccountsFetched(true);
};
// request tracker
let loading = false;
try {
// will only fetch accounts if we have id's and no accounts
if (!loading && idsFetched && !accountsFetched) {
fetchAccounts();
}
} catch (error) {
console.error(error);
} finally {
// cleanup => request tracker
return () => (loading = true);
}
}, [idsFetched, accountsFetched, setAccountsFetched]);
In this example the fetchIds function will fire one time, and then the fetchAccounts function will fire one time.
Another thing you have to be sure of, is that you have a piece of state like accountsFetched
that you use a condition on all your async operations to ensure that they only fire once. If you don't do this, while the async operation won't make multiple requests simultaneously, it will run repeatedly back to back.
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.