Custom hook to fetch API in React Js

Last updated : November 21, 2022

In this article, I will explain how to create a custom hook to make API calls in a React Js application. Creating a custom hook can significantly reduce the boilerplate code throughout my application.

Here is the complete code for the custom hook that can be used to call API endpoints. I use https://jsonplaceholder.typicode.com/ as my test endpoint.

import axios, { AxiosError } from 'axios';
import { useState } from "react";

interface AxiosRequestConfigProps {
    method: string
    url: string
}
export interface TitleSearchResponse {
    userId: number,
    id: number,
    title: string,
    completed: boolean,
}

export type ResponseType = TitleSearchResponse

type HookReturnArray = [
        (params: AxiosRequestConfigProps) => void, 
        ResponseType | null, 
        boolean, 
        string | null
    ]

export const useApiFetch = (): HookReturnArray => {

    const [data, setData] = useState<ResponseType | null>(null);
    const [error, setError] = useState<string | null>(null);
    const [loading, setLoading] = useState<boolean>(false);

    const fetchApiData = async (params: AxiosRequestConfigProps): Promise<void> => {
        setLoading(true)
        setError("")

        axios.request(params)
            .then((response) => {
                setData(response.data)
            })
            .catch((err: Error | AxiosError) => { alert(err.message)
                if (axios.isAxiosError(err)) {
                    setError(err.message)
                } else {
                    setError("Something went wrong")
                }
            }).finally(() => {
                setLoading(false)
            })

    }
    return [fetchApiData, data, loading, error]
}

The interface AxiosRequestConfigProps defines the request object. It includes the request method and the endpoint URL.

The interface TitleSearchResponse defines the response data structure. I assign the TitleSearchResponse interface to the type ResponseType, so I can use this hook to call a broader range of endpoints that return different formats of responses.

The hook returns an array that contains a function reference and variables. The function name allows me to call an API, and the variables deliver the response and related processing statuses.

Here is how to use the API hook.

import { useApiFetch, UserSearchResponse } from "./use_fetch_api";
const App = () => {
  const [fetchApiData, data, loading, error] = useApiFetch()
  
  const loadTitles = () => {
    fetchApiData(
      {
          method: "get",
          url: "https://jsonplaceholder.typicode.com/todos/1",
      }
    )
  }
  return (
    <div>
      <button onClick={() => loadTitles()}>Load data</button>
      {loading && "Loading"}
      {error && error}
      {data && 
       <div>
          <p>{data.id}</p>
          <p>{data.userId}</p>
          <p>{data.title}</p>
          <p>{data.completed ? "completed" : "not completed"}</p>
        </div>
      }
    </div>
  );
}
export default App;

So far, my API hook can call APIs that responses match the TitleSearchResponse interface. What if I want to call an API that returns a different type of response?

Here is how I implement an API that returns an array of objects. The UserSearchResponse is the interface for the new API response. I also have to change the data type of my hook's response.
export type ResponseType = TitleSearchResponse & UserSearchResponse[]

Here is the complete code.

import axios, { AxiosError } from 'axios';
import { useState } from "react";

interface AxiosRequestConfigProps {
    method: string
    url: string
}
export interface TitleSearchResponse {
    userId: number,
    id: number,
    title: string,
    completed: boolean,
}

export interface UserSearchResponse {
    id: number,
    name: string,
    username: string,
    email: string,
    address: {
      street: string,
      suite: string,
      city: string,
      zipcode: string,
      geo: {
        lat: string,
        lng: string
      }
    }
}

export type ResponseType = TitleSearchResponse & UserSearchResponse[]

type HookReturnArray = [
        (params: AxiosRequestConfigProps) => void, 
        ResponseType | null, 
        boolean, 
        string | null
    ]

export const useApiFetch = (): HookReturnArray => {
    const [data, setData] = useState<ResponseType | null>(null);
    const [error, setError] = useState<string | null>(null);
    const [loading, setLoading] = useState<boolean>(false);

    const fetchApiData = async (params: AxiosRequestConfigProps): Promise<void> => {
        setLoading(true)
        setError("")

        axios.request(params)
            .then((response) => {
                setData(response.data)
            })
            .catch((err: Error | AxiosError) => {
                if (axios.isAxiosError(err)) {
                    setError(err.message)
                } else {
                    setError("Something went wrong")
                }
            }).finally(() => {
                setLoading(false)
            })
    }
    return [fetchApiData, data, loading, error]
}

Here is how I can call the custom hook that can handle multiple API response types.

import { useApiFetch, UserSearchResponse } from "./use_fetch_api";

const App = () => {
  const [fetchApiData, data, loading, error] = useApiFetch()
  const [fetchApiDataUsers, userData, userLoading, userError] = useApiFetch()

  const loadTitles = () => {
    fetchApiData(
      {
          method: "get",
          url: "https://jsonplaceholder.typicode.com/todos/1",
      }
    )
  }
  const loadUsers = () => {
    fetchApiDataUsers(
      {
        method: "get",
        url: "https://jsonplaceholder.typicode.com/users",
      }
    )
  }
  return (
    <div>
      <button onClick={() => loadTitles()}>Load data</button>
      {loading && "Loading"}
      {error && error}
      {data && 
       <div>
          <p>{data.id}</p>
          <p>{data.userId}</p>
          <p>{data.title}</p>
          <p>{data.completed ? "completed" : "not completed"}</p>
        </div>
      }

      <button onClick={() => loadUsers()}>Load data</button>
      {userLoading && "Loading"}
      {userError && error}
      {userData?.map((user: UserSearchResponse) => {
          return  <div>
                    <p>{user.name}</p>
                    <p>{user.email}</p>
                    <p>{user.address.city}</p>
                  </div>
      })}
    </div>
  );
}
export default App;
Lance
By: Lance
Lance is a software engineer with over 15 years of experience in full-stack software development.
Read more...

Comments are disabled

No Comments