Skip to content

Async Effect Gotcha

Suppose we want to fetch some data on mount, and we don't want to use SWR. We'll make a fetch request inside a useEffect hook.

Using the async/await syntax 👀, most developers would try to do something like this:

React.useEffect(async () => {
const response = await fetch(ENDPOINT);
const json = await response.json();
setTemperature(json.temperature);
}, []);

In order to use the await keyword, we need to be within an async function. It seems logical that we'd make our effect callback async, but unfortunately, it doesn't work:

Code Playground

import React from 'react';

const ENDPOINT = 'https://jor-test-api.vercel.app/api/get-temperature';

function App() {
const [temperature, setTemperature] = React.useState(null);

React.useEffect(async () => {
const response = await fetch(ENDPOINT);
const json = await response.json();

setTemperature(json.temperature);
}, []);

return (
<p>
Current temperature:
{typeof temperature === 'number' && (
<span className="temperature">
{temperature}°C
</span>
)}
</p>
);
}

export default App;
preview

Something went wrong

destroy is not a function

Lint Warning

  • Effect callbacks are synchronous to prevent race conditions. Put the async function inside: useEffect(() => { async function fetchData() { // You can await here const response = await MyAPI.getData(someId); // ... } fetchData(); }, [someId]); // Or [] if effect doesn't need props or state Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching

    Rule: react-hooks/exhaustive-deps

    Location: Line 8, Column 19

console
  1. destroy is not a function
  2. Warning: useEffect must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately: useEffect(() => { async function fetchData() { // You can await here const response = await MyAPI.getData(someId); // ... } fetchData(); }, [someId]); // Or [] if effect doesn't need props or state Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching at App (https://sandpack-bundler.vercel.app/App.js:24:42)
  3. Warning: useEffect must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately: useEffect(() => { async function fetchData() { // You can await here const response = await MyAPI.getData(someId); // ... } fetchData(); }, [someId]); // Or [] if effect doesn't need props or state Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching
  4. The above error occurred in the <App> component: at App (https://sandpack-bundler.vercel.app/App.js:24:42) Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
  5. Could not consume error:
    [[Error]]
  6. destroy is not a function
  7. The above error occurred in the <App> component: at App (https://sandpack-bundler.vercel.app/App.js:24:42) Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
  8. Could not consume error:
    [[Error]]
  9. Could not consume error:
    [[Error]]

In addition to the cryptic error message, we also see the following warning in the console:

Warning: useEffect must not return anything besides a function, which is used for clean-up.

It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately.

We're not allowed to make the effect callback async. Instead, we need to create another function within the effect. Here's how I typically solve this problem:

React.useEffect(() => {
// Create an async function within our effect:
async function runEffect() {
const response = await fetch(ENDPOINT);
const json = await response.json();
setTemperature(json.temperature);
}
// Immediately call this function to run the effect:
runEffect();
}, []);

Essentially, we move all of our effect logic into this async function. I like to use a generic name like runEffect instead of a specific name like fetchTemperature, to make it clear that we're moving everything related to the effect into this function.

If our effect has a cleanup function, that cleanup function should not be included in our runEffect function:

React.useEffect(() => {
async function runEffect() {
// ... Effect logic here
}
runEffect();
return () => {
// ... Cleanup logic here
}
}, []);

To be clear: we don't run into this issue at all when we use a data-fetching library like SWR. This is one of many problems that those tools solve for us!

But I wanted to cover this anyway because not everyone uses data-fetching libraries. And even if you do, there may still be cases where you want to await some sort of async operation unrelated to data fetching.