api-client.ts : axios interceptor
import axios, { type AxiosRequestConfig, type AxiosResponse, AxiosError } from 'axios';
import HttpStatus from 'http-status-codes';
import { toast } from 'react-toastify';
const BASE_URL = 'your-base-url';
// ===========> Axios instance <============
const apiClient = axios.create({
baseURL: BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// ====== Throttle Network Error Toast (once every 60 seconds) ======
const TOAST_THROTTLE_TIME = 60000;
const hasNetworkErrorToastShownRecently = () => {
const lastShown = sessionStorage.getItem('lastNetworkErrorToast');
return lastShown && Date.now() - Number(lastShown) < TOAST_THROTTLE_TIME;
};
const setNetworkErrorToastTimestamp = () => {
sessionStorage.setItem('lastNetworkErrorToast', Date.now().toString());
};
let loginToastShown = false;
// ==========> Interceptor <================
apiClient.interceptors.response.use(
(response: AxiosResponse) => response.data,
(error: AxiosError) => {
const status = error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR;
const message = HttpStatus.getStatusText(status) || 'An unknown error occurred';
if (status === HttpStatus.FORBIDDEN) {
// some logout logic or maybe you want to do something different.
}
return Promise.reject({ status, message, data: null });
}
// Handle network errors (only show once every 60 seconds)
if (!error.response || error.code === 'ECONNABORTED') {
if (!hasNetworkErrorToastShownRecently()) {
toast.error('Network error! Please check your internet connection.');
setNetworkErrorToastTimestamp();
}
return Promise.reject({ status, message, data: null });
}
const data: any = error.response?.data;
if (data && !data.success && Array.isArray(data.errors)) {
console.error('Validation Errors:', data.errors);
data.errors.forEach((err: any) => {
toast.error(err.msg || 'Validation error');
});
return;
}
return Promise.reject({
status,
message,
data: error.response?.data || null,
});
}
);
export const apiRequest = async (
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
url: string,
data?: any,
config: AxiosRequestConfig = {}
) => {
try {
const response = await apiClient({
method,
url,
data,
...config,
});
return response;
} catch (error) {
throw error;
}
};
export default apiClient;
useAxios.ts : a hook which exposes data, loading and error and lot of options.
import { useState, useEffect, useCallback } from "react";
import { AxiosError } from "axios";
import { apiRequest } from "../lib";
import { type UseAxiosProps, type UseAxiosReturn } from "../types";
import { GlobalVariable } from "../global";
export const useAxios = <T>({
method = "GET",
url,
body,
config = {},
headers = {},
autoFetch = false,
shouldFetch = true,
}: UseAxiosProps): UseAxiosReturn<T> => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<AxiosError | null>(null);
const fetchData = useCallback(
async (overrideBody: object | null = null, overrideHeaders: Record<string, string> = {}, overrideUrl: string | null = null) => {
if (!shouldFetch) return null;
if (!(["/login"].includes(url?.trim() || "")) && !(localStorage.getItem(GlobalVariable.EXECUTIVE_TOKEN_PATH))) return;
setLoading(true);
setError(null);
try {
const response = await apiRequest(method, overrideUrl || url || "", overrideBody || body, {
...config,
headers: {
...((url !== '/login') && { Authorization: `Bearer ${localStorage.getItem(GlobalVariable.EXECUTIVE_TOKEN_PATH)}`}),
...headers,
...overrideHeaders,
},
});
setData(response as T);
return response as T;
} catch (err) {
setError(err as AxiosError);
// @ts-ignore
return err.data;
} finally {
setLoading(false);
}
},
[method, url, body, config, headers, shouldFetch]
);
useEffect(() => {
if (autoFetch && shouldFetch) {
fetchData();
}
}, [autoFetch, shouldFetch, url, method]);
return { data, loading, error, fetchData };
};
types.ts : basic types
import { AxiosError, type AxiosRequestConfig } from "axios";
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
export interface UseAxiosProps {
method?: HttpMethod;
url?: string;
body?: object;
config?: AxiosRequestConfig;
headers?: Record<string, string>;
autoFetch?: boolean;
shouldFetch?: boolean;
scope ?: string;
}
export interface UseAxiosReturn<T> {
data: T | null;
loading: boolean;
error: AxiosError | null;
fetchData: (overrideBody?: object, overrideHeaders?: Record<string, string>, overrideUrl?: string) => Promise<T | null>;
}