Custom hook to fetch API in React Js

Last updated : Jul 30, 2023 12:00 AM

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

Hi, I'm Lance Raney, a dedicated Fullstack Developer based in Oklahoma with over 15 years of exp

Read more...