Giorgio Polvara's Blog

Fetching Asynchronous Data with React Hooks

Photo by cubicroot XYZ
TL;DR;

Use the useAsync() hook from streamich to handle your asynchronous needs.

Fetching data is one of those things that I do all the time in my code. I've decided to take a look at ways to streamline it as much as possible.

A Word About My Setup

Before we get started, there are a few things that might throw you off.

  • I use async/await; you can read more about it here
  • I initialize my state with class fields rather than in the constructor
  • In the examples, I'm going to call getResource which is a function that returns a Promise. I do this both because I want to hide the fetching logic and because it's then easier to test my component—I jest.mock the import

Fetch One Resource

Probably the most common use-case for asynchronous code is to fetch a single resource when the component mounts. We need this all the time: fetch the latest tweets for the user, get the list of friends, fetch the most popular videos… the list goes on and on.

With class-based components we do this in the componentDidMount method:

import React from "react";
import { getResource } from "./api";

class FetchOneResource extends React.Component {
  state = { valueA: null };

  async componentDidMount() {
    const valueA = await getResource("A");
    this.setState({ valueA });
  }

  render() {
    const { valueA } = this.state;

    return valueA == null ? "Loading..." : valueA;
  }
}

We fetch our resource and put the result in the state. Our render method uses a ternary operator to decide if we want to show a loading message or our result.

The above code works but has two issues:

  • What if getResource() fails?
  • What if getResource() doesn't fail but returns null?

Those are usually considered edge cases but they can happen. We can't push this code in production unless we address these issues:

import React from "react";
import { getResource } from "./api";

class FetchOneResource extends React.Component {
  state = {
    valueA: null,
    loadingA: true,    errorA: null  };

  componentDidMount() {
    this.getA();  }

  async getA() {    try {      this.setState({ loadingA: true });      const valueA = await getResource("A");      this.setState({ valueA });    } catch (e) {      this.setState({ errorA: e });    } finally {      this.setState({ loadingA: false });    }  }
  render() {
    const { valueA, loadingA, errorA } = this.state;
    if (errorA) return "Failed to load resource A";    return loadingA ? "Loading..." : valueA;  }
}

We added two new state variables, loadingA and errorA. Note how loadingA is set to true initially. That's because we want to display the loading message already at the first render.

We also moved the fetching in a separate method and wrapped it in a try/catch statement. The finally branch is a somewhat new addition to JavaScript. It simply means "run this branch all the time no matter if the above code failed or not."


Nowadays, I avoid writing React classes if I can help it. Let's see how we can rewrite FetchOneResource using Hooks:

import React, { useState, useEffect } from "react";
import { getResource } from "./api";

function FetchOneResource() {
  const [valueA, setValueA] = useState(null);
  const [errorA, setErrorA] = useState(null);
  const [loadingA, setLoadingA] = useState(true);
  async function getA() {
    try {
      setLoadingA(true);
      const valueA = await getResource("A");
      setValueA(valueA);
    } catch (e) {
      setErrorA(e);
    } finally {
      setLoadingA(false);
    }
  }
  useEffect(() => {
    getA();
  }, []);

  if (errorA) return "Failed to load resource A";
  return loadingA ? "Loading..." : valueA;
}

Most of the code remained the same, so let's focus on the differences.

We converted our class into a function. That's because Hooks can be used only within a functional component.

Our state is now declared with the useState hook which gives us back the value and a function to set it. This is roughly how it compares to a class-based state:

// This code in a class component...
this.setState({ foo: 42 });
console.log(this.state.foo);

// ...is equivalent to this code in a functional component
const [foo, setFoo] = useState();
setFoo(42);
console.log(foo);

The other Hook that we're using is useEffect. It's probably the most complex of all pre-defined Hooks I wrote a bit about how it works here. In this case, we're using it to run our getA function for us precisely one time after the component mounts.


Before we move on to the next example, I would like to take a minute to refactor our component. The code as is written works fine but it's very verbose, I would like to make it easier to follow.

One advantage of Hooks is that they are composable and make it easy to extract logic in a separate function.

We can take advantage of this and move our code to fetch in a separate method:

import React, { useState, useEffect } from "react";
import { getResource } from "./api";

function useA() {
  const [valueA, setValueA] = useState(null);
  const [errorA, setErrorA] = useState(null);
  const [loadingA, setLoadingA] = useState(true);
  async function getA() {
    try {
      setLoadingA(true);
      const valueA = await getResource("A");
      setValueA(setValueA);
    } catch (e) {
      setErrorA(e);
    } finally {
      setLoadingA(false);
    }
  }
  useEffect(() => {
    getA();
  }, []);

  return [valueA, errorA, loadingA];}

function FetchOneResource() {
  const [valueA, errorA, loadingA] = useA();  if (errorA) return "Failed to load resource A";
  return loadingA ? "Loading..." : valueA;
}

Ah! Much better, don't you think? Our FetchOneResource is now only three lines long, and it's quite easy to understand what is going on.

Still, I think we can do better. But first, let's see how to fetch more than one asynchronous resource at the same time.

Fetch Multiple Resources at the Same Time

In many cases, you want to load more than one resource at the same time. Think of a page that renders a list of posts and your followers.

Let's see how we could do it with Hooks. First a naive implementation:

import React, { useState, useEffect } from "react";
import { getResource } from "./api";

function useA() {
  const [valueA, setValueA] = useState(null);
  const [errorA, errorA] = useState(null);
  const [loadingA, setLoadingA] = useState(true);
  async function getA() {
    try {
      setLoadingA(true);
      const valueA = await getResource("A");
      setValueA(valueA);
    } catch (e) {
      setErrorA(a);
    } finally {
      setLoadingA(false);
    }
  }
  useEffect(() => {
    getA();
  }, []);

  return [valueA, errorA, loadingA];
}

function useB() {
  const [valueB, setValueB] = useState(null);
  const [errorB, setErrorB] = useState(null);
  const [loadingB, setLoadingB] = useState(true);
  async function getB() {
    try {
      setLoadingB(true);
      const valueB = await getResource("B");
      setValueB(valueB);
    } catch (e) {
      setErrorB(a);
    } finally {
      setLoadingB(false);
    }
  }
  useEffect(() => {
    getB();
  }, []);

  return [valueB, errorB, loadingB];
}

function FetchMultipleResourceAtOnce() {
  const [valueA, errorA, loadingA] = useA();
  const [valueB, errorB, loadingB] = useB();

  return (
    <div>
      {errorA
        ? "Failed to load resource A"
        : loadingA
        ? "Loading A..."
        : valueA}
      {errorB
        ? "Failed to load resource B"
        : loadingB
        ? "Loading B..."
        : valueB}
    </div>
  );
}

We're duplicating a lot of code, but it's quite easy to fix it. useA and useB are basically the same function. The only difference is that they are passing different arguments to getResource. Let's fix that:

import React, { useState, useEffect } from "react";
import { getResource } from "./api";

function useAsync(getMethod, params) {
  const [value, setValue] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  async function getResource() {
    try {
      setLoading(true);
      const result = await getMethod(...params);
      setValue(result);
    } catch (e) {
      setError(e);
    } finally {
      setLoading(false);
    }
  }
  useEffect(() => {
    getResource();
  }, params);

  return { value, error, loading };
}

function FetchMultipleResourceAtOnce() {
  const resourceA = useAsync(getResource, ["A"]);  const resourceB = useAsync(getResource, ["B"]);
  return (
    <div>
      {resourceA.error
        ? "Failed to load resource A"
        : resourceA.loading
        ? "Loading A..."
        : resourceA.value}
      {resourceB.error
        ? "Failed to load resource B"
        : resourceB.loading
        ? "Loading B..."
        : resourceB.value}
    </div>
  );
}

We made a generic useAsync Hook that takes two parameters: the method to call and the list of parameters to call it with.

Note that we are now passing params to useEffect so that if one of them changes we fetch the resource again.

We're also not returning an array anymore but an object. I think an object is easier to handle because it creates only one variable and doesn't force us to remember the order in which the results are returned.

The last thing left to do is to take useAsync and move it in a separate file so that we can use it in other components too. Luckily a version of useResource that works similarly to ours is available as part of react-use.

import React from "react";
import { getResource } from "./api";
import { useAsync } from "react-use";
function FetchMultipleResourceAtOnce() {
  const resourceA = useAsync(getResource, ["A"]);
  const resourceB = useAsync(getResource, ["B"]);

  return (
    <div>
      {resourceA.error
        ? "Failed to load resource A"
        : resourceA.loading
        ? "Loading A..."
        : resourceA.value}
      {resourceB.error
        ? "Failed to load resource B"
        : resourceB.loading
        ? "Loading B..."
        : resourceB.value}
    </div>
  );
}

You can find useAsync here in case you want to use it in your projects. The actual implementation is a bit more complex than the one presented in this post so make sure you check out the README.

Would you like to have a civil discussion about this post? Hit me up on twitter.