Simplifying API Calls in React with Custom Hooks
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