How to use React Hooks to Abstract API Calls

16.07.20197 Min Read — In React JS

Calling APIs can be quite cumbersome in React.

First, it takes time to return the result. So, we will have to account for an intermediate loading state.

Second, we will have to use JavaScript promises, or async/await to resolve the promise.

Third, we also have to account for potential error and show our user the relevant error message.

We always have to account for these 3 actions when calling APIs. We can keep our code DRY by abstracting these 3 actions so that we can deal with the resulting data and state.

There are many ways to achieve this abstraction. You can use a separate class or higher-order components (HOCs). In this article, we will share how to use React hooks to abstract API calls.

Introducing React Hooks

Since React 16.8, React has included React Hooks. But what are hooks?

Hooks are functions that let you “hook into” React state and lifecycle features from function components. Hooks don’t work inside classes — they let you use React without classes.

Source

Before hooks, if we want to manage states inside a component, we can use:

  • Class components
  • Higher-Order Components (HOCs) or Recompose
  • Render Props

But, class components are confusing with the binding and this. And HOCs make your code harder to follow and test. You can read more on the motivation of introducing hooks.

Using React Hooks, you can manage states in functional components. React provides some built-in hooks such as useState, useEffect, and many others. We can also write custom hooks.

To abstract API calls, we will be using custom hooks.

An Example

Let say you want to call the Github API to search for a list of relevant repos, and then rendered the data. Using useState and useEffect, we have the following:

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

const App = () => {
  const [repos, setRepos] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const url =
      "https://api.github.com/search/repositories?q=react&sort=stars&order=desc";

    axios
      .get(url)
      .then(({ data: { items } }) => {
        setRepos(items);
        setIsLoading(false);
      })
      .catch(() => {
        setError("Something went wrong");
      });
  }, []);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <p>Sample Github API</p>
      {error && <div>{error}</div>}

      {repos.map(repo => (
        <div
          key={`repo-${repo.id}`}
          style={{ marginTop: 20, borderTop: "1px solid #e3e3e3" }}
        >
          <p>
            Name: {repo.name} (<a href={repo.html_url}>Github</a>)
            <br />
            <span>Stars: {repo.stargazers_count}</span>
          </p>
        </div>
      ))}
    </div>
  );
};

export default App;

Seems quite a lot of code to do something simple.

Not only that.

Imagine you need to make an API call in another page. You will have to write the loading state, resolving promises, and error handling again.

Basic React Custom Hooks

To begin abstraction, we first create a custom hook useApi:

// ./src/hooks.js

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

export const useApi = url => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    axios
      .get(url)
      .then(({ data }) => {
        setData(data);
        setIsLoading(false);
      })
      .catch(() => {
        setError("Something went wrong");
      });
  });

  return [isLoading, data, error];
};

Now, you can shorten your component:

import React from "react";
import { useApi } from "./hooks";

const App = () => {
  const url =
    "https://api.github.com/search/repositories?q=react&sort=stars&order=desc";
  const [isLoading, data, error] = useApi(url);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  const repos = error ? [] : data.items;

  return (
    <div>
      <p>Sample Github API</p>
      {error && <div>{error}</div>}

      {repos.map(repo => (
        <div
          key={`repo-${repo.id}`}
          style={{ marginTop: 20, borderTop: "1px solid #e3e3e3" }}
        >
          <p>
            Name: {repo.name} (<a href={repo.html_url}>Github</a>)
            <br />
            <span>Stars: {repo.stargazers_count}</span>
          </p>
        </div>
      ))}
    </div>
  );
};

export default App;

You can reuse the useApi custom hook in another component when you need to call an external API. All you have to do is to pass in the url.

However, there are three issues with the current code:

  1. The App component shouldn't need to determine what URL to call to get the data. We should abstract the API calling functions out of the component.
  2. It seems like the current solution only works with GET requests. Can I abstract useApi further to a POST request and pass it parameters?
  3. What if I want to perform an action after we got the data?

Abstracting API Functions

First, we will create one file to house all the API calls:

// ./src/api.js
import axios from "axios";

const BASE_URL = "https://api.github.com";

export const searchRepos = params => {
  if (params) {
    const { query } = params;
    if (query) {
      const url = `${BASE_URL}/search/repositories?q=${query}&sort=stars&order=desc`;
      return axios.get(url);
    }
  }

  throw new Error("Must provide a query");
};

Then, we will use the searchRepos function in our App component:

// .src/app.js
import React from "react";
import { useApi } from "./hooks";
import { searchRepos } from "./api";

const App = () => {
  const query = { query: "react" };
  const [isLoading, data, error] = useApi(searchRepos, query);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  const repos = error ? [] : data.items;

  return (
    <div>
      <p>Sample Github API</p>
      {error && <div>{error}</div>}

      {repos.map(repo => (
        <div
          key={`repo-${repo.id}`}
          style={{ marginTop: 20, borderTop: "1px solid #e3e3e3" }}
        >
          <p>
            Name: {repo.name} (<a href={repo.html_url}>Github</a>)
            <br />
            <span>Stars: {repo.stargazers_count}</span>
          </p>
        </div>
      ))}
    </div>
  );
};

export default App;

We pass the searchRepos function and the query object into the useApi hook.

So we must update the useApi:

// ./src/hooks.js

import { useState, useEffect } from "react";

export const useApi = (apiFunction, params) => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    apiFunction(params)
      .then(({ data }) => {
        setData(data);
        setIsLoading(false);
      })
      .catch(() => {
        setError("Something went wrong");
        setIsLoading(false);
      });
  }, [apiFunction, params]);

  return [isLoading, data, error];
};

The useApi hook will call the apiFunction and pass in the params. Note that the API helper functions in api.js have to be consistent, i.e., taking in only one object.

As long as the API helper functions defined, you can use the useApi hook for both GET and POST requests.

Adding in the callback functions

We can abstract the useApi hook further by taking in a callback function as the third argument.

// ./src/hooks.js

import { useState, useEffect } from "react";

export const useApi = (apiFunction, params, callback) => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    apiFunction(params)
      .then(({ data }) => {
        setData(data);
        setIsLoading(false);
        if (callback) {
          callback(data);
        }
      })
      .catch(() => {
        setError("Something went wrong");
        setIsLoading(false);
      });
  }, [apiFunction, params, callback]);

  return [isLoading, data, error];
};

To use the callback feature, you have to pass in the callback function:

// ./src/App.js

// ... same as previous example

const App = () => {
  const query = { query: "react" };

  // callback function
  const someAction = data => {
    // do something
    return data;
  };

  const [isLoading, data, error] = useApi(searchRepos, query, someAction);

	// remaining code same as previous example
};

export default App;

Conclusion

React hooks allow you to reuse stateful behaviors between different components. While React provides you with some standard hooks, they might not be enough.

In this article, we have shared how to use custom hooks to abstract:

  • Resolving promises
  • Loading state
  • Handling errors

when calling APIs in React.

There are many other recipes which you can use React hooks. You can find some examples here. Do share with us if you find an interesting example.