1. Introduction
In any real-world React application, you’ll likely need to make network requests to a backend server. Often, you want to intercept these requests and responses to:
- Inject authentication headers (like JWT tokens)
- Show or hide a loading spinner during network activity
- Log all API requests and responses
- Handle common errors globally (like redirecting on a 401 Unauthorized)
- Retry failed requests
This is where interceptors come in.
Interceptors are functions that sit between your application and the actual network call. They allow you to hook into Axios’s request/response lifecycle and modify the request before it’s sent or the response before it’s processed.
Axios provides two types of interceptors:
- Request Interceptors – executed before the request is sent.
- Response Interceptors – executed after a response is received (or an error occurs).
This allows you to perform cross-cutting operations (like showing a loader or handling auth) in one central place, instead of duplicating logic in every component.
2. Implementation
Step 1: Install Axios
npm install axios
Step 2: Create a Centralized Axios Instance
We’ll create a custom Axios instance so we can attach interceptors and reuse it everywhere in the app.
src/api/axiosInstance.js
import axios from 'axios'; const axiosInstance = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com', timeout: 10000, // 10 seconds timeout }); export default axiosInstance;
Step 3: Add Request & Response Interceptors
Add global interceptors to the instance you just created. Here’s where we can:
- Attach auth headers
- Trigger a global loader
- Handle common errors like
401
,403
,500
- Log activity
let loaderSetter = null; // external function to update loader state export const registerLoaderSetter = (fn) => { loaderSetter = fn; }; axiosInstance.interceptors.request.use( (config) => { console.log(`[Request] ${config.method.toUpperCase()} ${config.url}`); // Simulate adding an auth token const token = localStorage.getItem('authToken'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } if (loaderSetter) loaderSetter(true); // start loader return config; }, (error) => { if (loaderSetter) loaderSetter(false); // stop loader console.error('[Request Error]', error); return Promise.reject(error); } ); axiosInstance.interceptors.response.use( (response) => { console.log(`[Response] ${response.config.url}`, response.status); if (loaderSetter) loaderSetter(false); // stop loader return response; }, (error) => { if (loaderSetter) loaderSetter(false); // stop loader console.error('[Response Error]', error); // Global error handling if (error.response?.status === 401) { alert('Unauthorized. Redirecting to login...'); window.location.href = '/login'; } if (error.response?.status === 500) { alert('Something went wrong on the server.'); } return Promise.reject(error); } );
Step 4: Create Loader Context for Global State
We’ll use React Context to track loading status across the app.
src/context/LoaderContext.jsx
import { createContext, useContext, useState } from 'react'; const LoaderContext = createContext(); export const LoaderProvider = ({ children }) => { const [loading, setLoading] = useState(false); return ( <LoaderContext.Provider value={{ loading, setLoading }}> {children} </LoaderContext.Provider> ); }; export const useLoader = () => useContext(LoaderContext);
Step 5: Setup App with Loader and Axios Integration
src/App.jsx
import { useEffect } from 'react'; import { LoaderProvider, useLoader } from './context/LoaderContext'; import { registerLoaderSetter } from './api/axiosInstance'; import Demo from './Demo'; const Loader = () => { const { loading } = useLoader(); return loading ? ( <div style={{ background: '#eee', padding: 10, position: 'fixed', top: 0, left: 0, width: '100%', textAlign: 'center' }}> 🔄 Loading... </div> ) : null; }; const AppWithLoader = () => { const { setLoading } = useLoader(); useEffect(() => { registerLoaderSetter(setLoading); // connect loader with interceptors }, [setLoading]); return ( <> <Loader /> <Demo /> </> ); }; const App = () => ( <LoaderProvider> <AppWithLoader /> </LoaderProvider> ); export default App;
Step 6: Make a Test Request in a Component
import axiosInstance from './api/axiosInstance'; const Demo = () => { const callApi = async () => { try { const response = await axiosInstance.get('/posts/1'); alert(`Title: ${response.data.title}`); } catch (error) { console.error('API call failed'); } }; return ( <div style={{ padding: 20 }}> <h2>Test API Call with Interceptors</h2> <button onClick={callApi}>Fetch Post</button> </div> ); }; export default Demo;
3. Best Practices
- Avoid duplicating interceptors inside components. Always attach them once globally.
- Use
axios.create()
to build a custom instance and reuse it everywhere. - Don’t forget to cancel or clean up interceptors if you ever attach them dynamically in a component (using
eject()
). - Track concurrent requests for more advanced loaders (so quick responses don’t flicker the loader).
- Log important metadata such as status codes, headers, or trace IDs if needed.