Correctly handling async/await in React components - Part 1

Context

In those blog posts I try to illustrate what problems async/await do present for React components and how we can fix them. :whitecheckmark:

If you have not checked the previous post yet, please do - so you get more context of what issues can appear with async code in React components: Correctly handling async/await in React components

As stated in the React community, not handling asyncronous code correctly can lead to a bugfest, so let's see how to deal with it properly. :bug::bug::bug:

The good

In the previous post here, we managed to fix the two issues we had - one about a React warning when unmounting a component before an async call was finished. And a second one about handling concurrent async calls so we always receive only the latest results for our API calls.

The bad

The bad part is that our code is now at aprox twice the initial size in lines of code and looks harder to read. Our initial component was pretty simple. You could simply see that it makes a call to an API and displays a string.

Now it does too many things - setting up hooks to see if it's mounted, creating cancellation tokens...

The refactored

I thought we might want to look at how we can refactor this component to make the code more readable.

The nice thing about React hooks is that you can extract them away from the components and even reuse them when you want.

The isMouted hook

const useIsMounted = () => {
  const isMounted = useRef(false);
  
  useEffect(() => {
    isMounted.current = true;
    return () => (isMounted.current = false);
  }, []);

  return isMounted;
};

The data fetching hook

const useJokeAsync = (componentIsMounted, more) => {
  const [joke, setJoke] = useState("");
  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 ;-)"
      );
    };
  }, [componentIsMounted, more]);

  return joke;
};

And now finally our component :star:

export default function RandomJoke({ more, loadMore }) {
  const componentIsMounted = useIsMounted();
  const joke = useJokeAsync(componentIsMounted, more);

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

Now this is much better, but can be improved

We had a a slight issue in our implementation - if you read the first post and this one until here, try to think for 1 min before you scroll down.

Well... if you said the componentIsMounted is redundant you are right :sunglasses:. Why? because all hooks cleanup functions are called on component unmount. That means the cancellation is called before any setState can be called. So having this accidental complexity avoided now we have:

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

const useJokeAsync = (more) => {
  const [joke, setJoke] = useState("");
  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;

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

        console.error(err);
      }
    }

    fetchJoke();

    return () => {
      console.log("Calling cleanup");

      // 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 joke;
};

export default function RandomJoke({ more, loadMore }) {
  const joke = useJokeAsync(more);

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

Conclusions

Extracting code into re-usable hooks can make sense a lot of times, both for the readability of the components and for the isolation of certain concerns like data fetching.

You can check out the code on Github.

If you like this post follow me on Twitter where I post more cool stuff about React and other awesome technologies. :fire::fire::fire:

This post is also available on DEV.