React in 2019 Part 1: The Custom useApi Hook

20 september 2019

This read is part of the series 'React in 2019'. This series is a combination of examples how I've recently coded. These can mostly be read and understood without knowledge of other parts

You've probably seen some useFetch custom hooks. If you haven't, don't worry (you could read this). I just wanted to share mine, which is a little different. In contrary to other useFetch methods, mine allows you to pass your own API function as an argument. This gives you more freedom and better separation of concerns. First, let's enumerate what this custom hook does for you.

The hook...

  1. Manages the response state (data, loading, error and success).
  2. Calls your own fetch function (axios for example), which you pass in as an argument.
  3. Can be used multiple times in the same React Component.
  4. Is independent of which HTTP method you use in the fetch function .

And optionally:

  1. Set an initial value for data. Especially Useful when using TypeScript.
  2. TypeScript.
  3. A small performance optimization.

This however, won't be covered in detail in this article.

The useApi hook

export function useApi(apiFunction) {
  const [response, setResponse] = React.useState({
    data: null,
    hasErrors: false,
    isFetching: false,
    isSuccess: false,
  })

  const fetchMethod = async () => {
    setResponse((prevState) => ({
      ...prevState,
      loading: true,
    }))
    try {
      const apiData = await apiFunction()
      setResponse({
        data: apiData,
        isFetching: false,
        hasErrors: false,
        isSuccess: true,
      })
    } catch (error) {
      setResponse({
        data: null,
        isFetching: false,
        hasErrors: true,
        isSuccess: false,
      })
    }
  }

  return [response, fetchMethod]
}

The big 'gotchas' in this hook are the fetchMethod and the apiFunction argument. You use the hook by calling fetchMethod, which executes the apiFunction you pass in as an argument.

  • data is the data that is returned from the API function. The initial value is set as null for now, but you could pass a defaultValue if you'd like (more on this later).
  • isFetching is basically the loading flag. Use this to show a loading page/spinner or whatever else you'd like.
  • hasErrors Just a boolean for demonstration purposes. There are multiple ways to do error handling and I will cover this in a separate article.
  • isSuccess boolean so we have an explicit way of telling the app that the API fetch was a success.

Example: Fetching users with the useApi hook

Using the hook is very straightforward. Notice how you don't have to use the same names as in the useApi hook.

import { fetchUsers } from '~/api/users'
import { useApi } from '~/hooks/useApi'

const UsersPage = () => {
  const [usersResponse, getUsers] = useApi(fetchUsers)

  React.useEffect(() => {
    getUsers()
  }, [getUsers])

  // Do what you like with these destructured properties.
  const { data, hasErrors, isFetching, isSuccess } = usersResponse

  if (hasErrors) {
    return null
  }

  return <>{data.map((user) => user.name)}</>
}

What are we doing here?

  1. We name the response object usersResponse, and the 'fetchMethod' part getUsers
  2. We pass the fetchUsers api function which is the ACTUAL HTTP function (more on this soon). Just remember this could be your axios function which has the HTTP method, API endpoint etc.
  3. We use the React.useEffect hook to fetch this data when the component/page mounts.
  4. Finally, we render the users.

You might be thinking: Ok cool ZakKa, but what makes this hook stand out? Let's take a break from this hook itself so I can explain some of my reasoning behind this approach.

Separation of concerns

Remember when I said:

In contrary to other useFetch methods, mine allows you to pass your own API function as an argument

Most of the time I do a lot of data validation and transforms before I return the data - or throw the errors 🤭 - from the HTTP request. I also don't want to mixup the reusable function for apiCalls with the hook. To demonstrate: I usually create a reusable function based on this, in combination with different API functions for each type of HTTP request:

/api/users.js

import { getClientReadyUser,validateUser } from '../models/user'
import { axiosRequest } from '.' // <- reusable axios function

async function fetchUser(userId) {
  try {
    const user = await axiosRequest('get', `/users/v1/${userId}`)
    const safeUser = validateUser(user) // data validation
    const clientReadyUser = getClientReadyUser(user) // data transforms

    return clientReadyProduct
  } catch (err) {
    throw err
  }
}

I want the code that's validating and transforming the data separate from the hook. We don't need all of that to be inside the 'React' side of the codebase. Same goes for AXIOS/ 'vanilla' fetch / or any other HTTP client. Also, same goes for the HTTP method (get/post/patch etc). I want to keep the hook simple! All of that needs to be handled before the data is received inside my precious useApi hook.

How I do all of that HTTP/data stuff is beyond the scope of this article. First, let's get back to the hook!

Posting a user with the useApi hook

So we've covered fetching, but what about posting? Not much difference, we'll add the code to the previous example:

import { fetchUsers, postUser } from "~/api/users"
import { useApi } from "~/hooks/useApi"

const UsersPage = () => {
  const [usersResponse, getUsers] = useApi(fetchUsers);
  const [createUserResponse, createUser] = useApi(() =>
    postUser({
      firstName: 'Musa',
      lastName: 'ibn Zakaria',
    }),
  );

  React.useEffect(() => {
    getUsers();
  }, [getUsers]);

  // Do what you like with these destructured properties.
  const { data: users, hasErrors, isFetching, isSuccess } = usersResponse;

  if (hasErrors || isFetching) {
    return null;
  };

  return (
    <>
      <ul>
        {users.map(user => <li key={user.id}>{user.name}<li>)}
      </ul>
      <button onClick={createUser}>Submit</button>
    </>
  );
};

As you can see, we can use the useApi hook multiple times without issue. If you are confused by data: users : This is optional and simply a cool way to rename destructured properties. It can be useful when you need data from multiple sources.

That's it! You don't need much more than this. I will cover other aspects like error handling, the actual API fetch functions and more to give you a more complete picture.

If you want to see a more advanced implementation, checkout the next chapter:

Optional: TypeScript, useCallBack and a custom initial value

The code I use myself has a bit more to it:

  • the React useCallBack hook for performance reasons.
  • TypeScript including generics to pass a type for the response data.
  • an initialDataValue as a second argument.
  • I separated the error message from the error data (the JSON or plain/text response). This was specific for the backend I was working with, you might not need that.

Check it out below and feel free to contact me if you have any questions about the code on my gist

The Hook with TypeScript and initialDataValue

import { ErrorType } from '~/core/api'

type ReturnType<DataType> = [
  {
    data: DataType;
    loading: boolean;
    isSuccess: boolean;
    errorMessage: ErrorType['message'];
    errorData: ErrorType['data'];
  },
  () => {} // <-- wip
];

export function useApi<T>(apiFunction, initialDataValue): ReturnType<T> {
  const [response, setResponse] = React.useState({
    data: initialDataValue,
    errorMessage: null,
    errorData: null,
    isFetching: false,
    isSuccess: false,
  })

  const fetchData = React.useCallback(async () => {
    setResponse((prevState) => ({
      ...prevState,
      loading: true,
    }))
    try {
      const apiData = await apiFunction()
      setResponse({
        data: apiData,
        isFetching: false,
        errorMessage: null,
        errorData: null,
        isSuccess: true,
      })
    } catch (error) {
      setResponse({
        data: null,
        isFetching: false,
        errorMessage: error.message,
        errorData: error.data,
        isSuccess: false,
      })
    }
  }, [apiFunction])

  return [response, fetchData]
}

Usage inside your component

type UsersType = {
  firstName: string;
  lastName: string;
};
const [usersResponse, getUsers] = useApi<UsersType[]>(fetchUsers, [])