Devtrium

How to use async functions in useEffect (with examples)

August 14, 20215 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!

Tip

You are using a linter right? If not, you really should!

· · ·
// ❌ 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);
}, [])
· · ·
Tip

If you want to know more about how the sleep function works, check out this article!

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])
· · ·
Info

Do you know why setData doesn't have to be included in the useCallback dependency array? If not, you can go over to the article on dependency arrays!

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 a catch 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

Package versions at the time of writing

Did you enjoy this article?

If so, a quick share on Twitter could really help out!