How to use async functions in useEffect (with examples)
August 14, 2021 • 5 min read
Introduction
useEffect
is usually the place where data fetching happens in React. Data fetching means using asynchronous functions, and using them in useEffect
might not be as straightforward as you'd think. Read on to learn more about it!
The wrong way
There's one wrong way to do data fetching in useEffect
. If you write the following code, your linter will scream at you!
· · ·
// ❌ don't do this
useEffect(async () => {
const data = await fetchData();
}, [fetchData])
· · ·
The issue here is that the first argument of useEffect
is supposed to be a function that returns either nothing (undefined
) or a function (to clean up side effects). But an async function returns a Promise, which can't be called as a function! It's simply not what the useEffect
hook expects for its first argument.
So how do you use asynchronous code inside a useEffect
?
Write the asynchronous function inside the useEffect
Usually the solution is to simply write the data fetching code inside the useEffect
itself, like so:
· · ·
useEffect(() => {
// declare the data fetching function
const fetchData = async () => {
const data = await fetch('https://yourapi.com');
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);
}, [])
· · ·
One caveat is that if you want to use the result from the asynchronous code, you should do so inside the fetchData
function, not outside. For example, the following would lead to issues:
· · ·
useEffect(() => {
// declare the async data fetching function
const fetchData = async () => {
// get the data from the api
const data = await fetch('https://yourapi.com');
// convert data to json
const json = await data.json();
return json;
}
// call the function
const result = fetchData()
// make sure to catch any error
.catch(console.error);;
// ❌ don't do this, it won't work as you expect!
setData(result);
}, [])
· · ·
Can you guess what the result
variable will be when the setData(result)
line is called?
One way to see this is to write a dummy asynchronous function that just waits for a certain amount of time.
· · ·
useEffect(() => {
// declare the async data fetching function
const fetchData = async () => {
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// waits for 1000ms
await sleep(1000);
return 'Hello World';
};
const result = fetchData()
// make sure to catch any error
.catch(console.error);;
// what will be logged to the console?
console.log(result);
}, [])
· · ·
What will get logged to the console?
If you got it right, congrats! result
will be holding a pending Promise object. In the console you'll see something like:
Promise {<pending>}
So how should you use the result of asynchronous code inside a useEffect
? Inside the fetch data function! To fix the above example, you would do it this way:
· · ·
useEffect(() => {
const fetchData = async () => {
await sleep(1000);
// this will log 'Hello Word' to the console
console.log('Hello World');
};
fetchData()
// make sure to catch any error
.catch(console.error);;
}, [])
· · ·
Translated with a setState
function, it looks like this:
· · ·
useEffect(() => {
// declare the async data fetching function
const fetchData = async () => {
// get the data from the api
const data = await fetch('https://yourapi.com');
// convert the data to json
const json = await response.json();
// set state with the result
setData(json);
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);;
}, [])
· · ·
What if you need to extract the function outside useEffect
?
In some cases you want to have the data fetching function outside useEffect
. In those cases you just have to be careful to wrap the function with a useCallback
.
Why? Well, since the function is declared outside of useEffect
, you will have to put it in the dependency array of the hook. But if the function isn't wrapped in useCallback
, it will update on every re-render, and thus trigger the useEffect
on every re-render. Which is rarely what you want!
· · ·
// declare the async data fetching function
const fetchData = useCallback(async () => {
const data = await fetch('https://yourapi.com');
setData(data);
}, [])
// the useEffect is only there to call `fetchData` at the right time
useEffect(() => {
fetchData()
// make sure to catch any error
.catch(console.error);;
}, [fetchData])
· · ·
Note on fetching data inside useEffect
I showed earlier an example of fetching data in useEffect
. Here it is as a reminder.
· · ·
useEffect(() => {
// declare the async data fetching function
const fetchData = async () => {
// get the data from the api
const data = await fetch('https://yourapi.com');
// convert the data to json
const json = await response.json();
// set state with the result
setData(json);
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);;
}, [])
· · ·
A reader (rightly) commented that you usually want a way to be able to cancel the setData
call. In the above example it's not useful because the call is done once and only once, but let's say the call depends on a param
:
· · ·
useEffect(() => {
// declare the async data fetching function
const fetchData = async () => {
// get the data from the api
const data = await fetch(`https://yourapi.com?param=${param}`);
// convert the data to json
const json = await response.json();
// set state with the result
setData(json);
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);;
}, [param])
· · ·
If param
changes value, fetchData
will be called twice. If this happens quickly, it's possible to have a race condition where the first call resolves after the second one, and thus the state will hold the older value.
The way to solve that issue is to have a variable which controls wether to update the state or not.
· · ·
useEffect(() => {
let isSubscribed = true;
// declare the async data fetching function
const fetchData = async () => {
// get the data from the api
const data = await fetch(`https://yourapi.com?param=${param}`);
// convert the data to json
const json = await response.json();
// set state with the result if `isSubscribed` is true
if (isSubscribed) {
setData(json);
}
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);;
// cancel any future `setData`
return () => isSubscribed = false;
}, [param])
· · ·
This is a very common pattern when fetching data in a useEffect
that might be triggered several times.
Wrap up
That's it, you now know how to properly use async functions in useEffect
hooks! Congrats 🎉
Note: Thanks a lot to readers on Reddit for their feedback that made this article better. Here are the major points that were improved thanks to them:
- The section Note on fetching data inside
useEffect
was added - The initial explanation of why you couldn't declare
useEffect
's callback as async was fixed (the initial version was incorrect) - The
fetchData
calls were added acatch
for errors. It's a bit more bloat but it's something that you absolutely should do in real apps, so I think it's worth it