Simplifying API Calls in React with Custom Hooks

Hasitha Chandula
4 min readFeb 27, 2024

Introduction

In modern web development, efficiently managing API requests is crucial for providing a seamless user experience. React’s custom hooks offer a powerful way to encapsulate and reuse logic across components. In this article, we explore two custom hooks, useQuery and useMutation, designed to handle data fetching and data mutation operations with ease. These hooks leverage Axios for making HTTP requests and are built with TypeScript for type safety and better developer experience.

Setting Up Axios with Interceptors

Before diving into the hooks, let’s set up an Axios instance with request and response interceptors to manage API tokens and handle 401 Unauthorized errors gracefully.

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

import { API_URL_V1 } from '@/utils/constants';

interface CustomAxiosRequestConfig extends AxiosRequestConfig {
_retry?: boolean;
}

const http: AxiosInstance = axios.create({
baseURL: API_URL_V1,
timeout: 30000 * 2,
});

async function getAccessTokenUsingRefreshToken(): Promise<string> {
// Implemet logic to get new access token using refresh token
return '';
}

let isRefreshing = false;
let failedQueue: ((token: string) => void)[] = [];

http.interceptors.request.use(async (config) => {
config.headers.authorization = `Bearer ${localStorage.getItem(
'ACCESS_TOKEN'
)}`;
return config;
});

http.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
async (error) => {
const originalRequest: CustomAxiosRequestConfig = error.config;

originalRequest.headers = {
...originalRequest.headers,
};

if (error.response.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
try {
const accessToken = await new Promise<string>((resolve) => {
failedQueue.push((newAccessToken) => {
resolve(newAccessToken);
});
});

originalRequest.headers.authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (error) {
// If getting a new access token using refresh token also fails, show an alert message
console.log('Failed to refresh access token using refresh token');
// You can show an alert message here
}
} else {
isRefreshing = true;
originalRequest._retry = true;

const newAccessToken = await getAccessTokenUsingRefreshToken();

isRefreshing = false;
localStorage.setItem('ACCESS_TOKEN', newAccessToken);

originalRequest.headers.authorization = `Bearer ${newAccessToken}`;
failedQueue.forEach((prom) => prom(newAccessToken));
failedQueue = [];

return axios(originalRequest);
}
}

return Promise.reject(error.response);
}
);

export default http;

The useQuery Hook

The useQuery hook is designed for fetching data. It manages loading states, errors, and optionally, pagination metadata.

Hook Definition

import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';

import http from '@/services/http';

export interface Meta {
totalPages: number;
total: number;
}

interface IProps {
url: string;
disableOnLoad?: boolean;
customHeaders?: object;
page?: number;
pageSize?: number;
}

function useQuery<Resource>({
url,
disableOnLoad,
customHeaders = {},
}: IProps) {
const [data, setData] = useState<Resource | null>(null);
const [meta, setMeta] = useState<Meta | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<unknown | null>(null);

useEffect(() => {
if (!disableOnLoad) {
fetchData();
}
}, []);

const fetchData = async () => {
setLoading(true);
setData(null);
setError(null);
const headers = {
headers: {
...customHeaders,
},
};
try {
const response = await http.get(url, headers);
setLoading(false);
setData(response?.data.data);
if (response.data?.meta) {
setMeta({
total: response.data?.meta?.total,
totalPages: response.data?.meta?.totalPages,
});
} else {
setMeta({
total: 0,
totalPages: 0,
});
}
return {
success: true,
data: response?.data,
errors: null,
};
} catch (err: any) {
setLoading(false);
setError(err);
setMeta({
total: 0,
totalPages: 0,
});
if (err.status === 403) {
toast.error("Don't have permissions");
}
return {
success: false,
data: null,
errors: err,
};
}
};

return { error, loading, data, retry: fetchData, meta };
}

export { useQuery };

Usage Example

const { data, loading, error } = useQuery<MyDataType>({ url: '/data' });

This hook simplifies fetching data and handling loading states, and errors automatically.

The useMutation Hook

The useMutation hook handles creating, updating, or deleting data. It's versatile, supporting custom HTTP methods and headers.

Hook Definition

import { useState } from 'react';
import { toast } from 'react-toastify';
import { AxiosError } from 'axios';

import { HTTP_TYPES } from '@/utils/constants';
import http from '@/services/http';

interface IProps {
url: string;
}

function useMutation<Resource>({ url }: IProps) {
const [loading, setLoading] = useState(false);

const mutate = async (
body: object,
method?: HTTP_TYPES,
customHeaders?: object,
requestUrl?: string
) => {
setLoading(true);

const headers = {
headers: {
...customHeaders,
},
};
try {
const response = await http[method || HTTP_TYPES.POST](
requestUrl || url,
body,
headers
);
setLoading(false);
return {
success: true,
data: response?.data?.data as Resource,
errors: null,
};
} catch (error) {
const err = error as AxiosError;
setLoading(false);
if (err.status === 403) {
toast.error("Don't have permissions");
}
return {
success: false,
data: null,
errors: err,
};
}
};

return { loading, mutate };
}

export { useMutation };

Usage Example

const { mutate, loading } = useMutation<MyDataType>({ url: '/data' });

const handleSubmit = async (data: MyDataType) => {
const result = await mutate(data, HTTP_TYPES.POST);
if (result.success) {
// Handle success
} else {
// Handle error
}
};

The useMutation hook streamlines data mutations, offering a simple interface for complex operations.

Conclusion

The useQuery and useMutation hooks abstract away the complexities of API interactions in React applications. By encapsulating Axios requests within these hooks, developers can focus on building their application logic rather than dealing with the intricacies of HTTP requests and state management. Using TypeScript generics ensures that these hooks are both flexible and type-safe, accommodating various data types without compromising on code quality or developer experience.

Incorporating these hooks into your React project not only simplifies data fetching and mutation operations but also enhances the maintainability and scalability of your codebase. Whether building a simple application or a complex web platform, these custom hooks are invaluable tools in your React development arsenal.

GitHub URL: https://github.com/Hasi6/react-hooks-for-handle-http-requests

--

--

Hasitha Chandula

Senior Full Stack Engineer specializing in TypeScript & Go. I create scalable web apps, love learning new tech, and thrive on teamwork.