DEV Community

Cover image for Correctly handling async/await in React components
Alexandru-Dan Pop
Alexandru-Dan Pop

Posted on • Updated on • Originally published at blog.alexandrudanpop.dev

Correctly handling async/await in React components

Context

There have been tweets lately stating that async/await does not work well with React components, unless there is a certain amount of complexity in how you deal with it.

Why is it so complex?

Handling asynchronous code is complex both in React and probably in most other UI libraries / frameworks. The reason is that at any time we are awaiting for some asynchronous code to finish, the component props could be updated or the component could be unmounted.

Exposing the problems

As the first tweet states, this is complex, but I'll try to explain what happens here.

In the following code snippets, we will look at a component making asynchronous HTTP requests using the axios library:

import React, { useState, useEffect } from "react";
import axios from "axios";

export default function RandomJoke({ more, loadMore }) {
  const [joke, setJoke] = useState("");

  useEffect(() => {
    async function fetchJoke() {
      try {
        const asyncResponse = await axios("https://api.icndb.com/jokes/random");
        const { value } = asyncResponse.data;
        setJoke(value.joke);
      } catch (err) {
        console.error(err);
      }
    }

    fetchJoke();
  }, [more]);

  return (
    <div>
      <h1>Here's a random joke for you</h1>
      <h2>{`"${joke}"`}</h2>
      <button onClick={loadMore}>More...</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Well...What issues does the above component have?

1) If the component is unmounted before the async request is completed, the async request still runs and will call the setState function when it completes, leading to a React warning 😕:
Alt Text

2) If the "more" prop is changed before the async request completes then this effect will be run again, hence the async function is invoked again. This can lead to a race condition if the first request finishes after the second request.

Alt Text

This could be wrong as we want to have the result of the latest async call that we requested.

Obviously in an app of this simplicity it would be ok, but let's say you had an app that queries an API based on some search text - you would always want to display the result of the latest query being typed.

How to fix

Issue no 1 - fix the React warning using a ref:

import React, { useState, useEffect, useRef } from "react";
import axios from "axios";

export default function RandomJoke({ more, loadMore }) {
  const [joke, setJoke] = useState("");
  const componentIsMounted = useRef(true);

  useEffect(() => {
    // each useEffect can return a cleanup function
    return () => {
      componentIsMounted.current = false;
    };
  }, []); // no extra deps => the cleanup function run this on component unmount

  useEffect(() => {
    async function fetchJoke() {
      try {
        const asyncResponse = await axios("https://api.icndb.com/jokes/random");
        const { value } = asyncResponse.data;

        if (componentIsMounted.current) {
          setJoke(value.joke);
        }
      } catch (err) {
        console.error(err);
      }
    }

    fetchJoke();
  }, [more]);

  return (
    <div>
      <h1>Here's a random joke for you</h1>
      <h2>{`"${joke}"`}</h2>
      <button onClick={loadMore}>More...</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see, what we did above was adding a ref componentIsMounted that simply updates when the component is unmounted. For this we added the extra effect with a cleanup function. Then where we fetch the data before setting the state, we check if the component is still mounted. Problem solved ✅! Now let's fix:

Issue no 2: fix the actual async issue. What we want is if we requested some async work to happen, we need a way to cancel it in case it didn't complete and meanwhile someone requested it again. Luckily axios has exactly what we need - a Cancellation Token 💥

import React, { useState, useEffect, useRef } from "react";
import axios, { CancelToken } from "axios";

export default function RandomJoke({ more, loadMore }) {
  const [joke, setJoke] = useState("");
  const componentIsMounted = useRef(true);

  useEffect(() => {
    // each useEffect can return a cleanup function
    return () => {
      componentIsMounted.current = false;
    };
  }, []); // no extra deps => the cleanup function run this on component unmount

  useEffect(() => {
    const cancelTokenSource = CancelToken.source();

    async function fetchJoke() {
      try {
        const asyncResponse = await axios(
          "https://api.icndb.com/jokes/random",
          {
            cancelToken: cancelTokenSource.token,
          }
        );
        const { value } = asyncResponse.data;

        if (componentIsMounted.current) {
          setJoke(value.joke);
        }
      } catch (err) {
        if (axios.isCancel(err)) {
          return console.info(err);
        }

        console.error(err);
      }
    }

    fetchJoke();

    return () => {
      // here we cancel preveous http request that did not complete yet
      cancelTokenSource.cancel(
        "Cancelling previous http call because a new one was made ;-)"
      );
    };
  }, [more]);

  return (
    <div>
      <h1>Here's a random joke for you</h1>
      <h2>{`"${joke}"`}</h2>
      <button onClick={loadMore}>More...</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What happens here:
1) We create a cancel token source every time the effect that fetches async data is called, and pass it to axios.
2) If the effect is called again before the async work is done, we take advantage of React's useEffect cleanup function. The cleanup will run before the effect is invoked again, hence we can do the cancellation by calling cancelTokenSource.cancel().

Conclusions

So yeah, handling async work in React is a bit complex. Of course we can abstract it by using a custom hook to fetch the data.

You might not always have to worry about those issues in every situation. If your component is well isolated, meaning it does not depend on prop values for the asynchronous code it runs, things should be ok... You will probably still get the unmount issue from time to time, and you should probably fix that as well if your component un-mounts often.

Correctly handling async/await in React components - Part 2

Top comments (11)

Collapse
 
zeeshan4242 profile image
Zeeshan

Thanks Alex. I was struggling with memory-leak issues. It really helped!

Collapse
 
alexandrudanpop profile image
Alexandru-Dan Pop

Glad to hear that!

Collapse
 
fefitin profile image
Fe

Very interesting, I hadn’t come across those issues until I read your post. Quick question: why are you using a ref instead of a regular state var in your first fix? Thanks!

Collapse
 
alexandrudanpop profile image
Alexandru-Dan Pop

By state var you mean having a setState({isUnmounted:true}) in the cleanup function of the first useEffect?

Don't think that will work, it might complain with the same: Cannot setState on unmounted warning. It seems refs are kept around even after Component unmounts, that's why they work in this case.

The React docs are a bit confusing, because they state refs live the same lifetime as components but obviously they stick around at least for as long as your async code still runs.

Collapse
 
fefitin profile image
Fe

Great, that's good to know, thanks!

Collapse
 
monfernape profile image
Usman Khalil • Edited

This is gold Alex. I've been on React for an year now and it's very helpful

Collapse
 
alexandrudanpop profile image
Alexandru-Dan Pop

Thank you!

Collapse
 
performautodev profile image
performautodev

You are awesome !

Collapse
 
mousticke profile image
Akim (mousticke.eth) @Colossos

Nice post. It's really helpful.
I'm just starting to learn the hooks system.
I know I can just google it but why would you use ref instead of state for componentIsMounted ? What is the purpose ?

Collapse
 
jamesthomson profile image
James Thomson

Ref's don't cause the component to re-render so you can update the value without side effects - in this case, the side effect being an unwanted state update (due to the resolving async call) that occurs after the component has actually been in an unmounted.

Collapse
 
sandeep194920 profile image
Sandeep194920

Really like the content here, good work! May I ask which theme you're using for the code? I'm using vscode and tried searching for such a theme but couldn't find one. Can you please let me know?