Search code examples
reactjsreact-nativeaxios

Behavior of axios interceptors


I have an app that uses axios interceptors to update the refresh token and access token. Everything works normally, but I have a question about the behavior of the axios interceptors.

I have the following custom hook that I use when I need to access my Rest API. Note that only when assembling the hook do I specify the interceptors.

import { useEffect } from 'react';
import axios from 'axios';
import { useDispatch } from 'react-redux';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { getData } from '../common';
import { login } from '../store/userSlice';

const axiosPrivate = axios.create({
    baseURL: 'https://xxx.xxx.com.br',
});

export default useAxiosPrivate = () => {

    const dispatch = useDispatch();

    useEffect(() => {
        const requestIntercept = axiosPrivate.interceptors.request.use(
            async (config) => {
                //******************** WATCH HERE *************************
                console.log(config.url);
                //*********************************************************
                const { accessToken } = await getData();
                config.headers['authorization'] = accessToken;
                return config;
            }, (error) => Promise.reject(error)
        );

        const responseIntercept = axiosPrivate.interceptors.response.use(
            response => response,
            async (error) => {
                const prevRequest = error?.config;
                if (error.response?.status === 401 && error.response.data?.code === 'AccessTokenExpired' && !prevRequest?.sent) {
                    prevRequest.sent = true;
                    const { refreshToken } = await getData();
                    try {
                        const { data } = await axios.post('https://xxx.xxx.com.br/auth/refresh-token', { refreshToken });
                        await AsyncStorage.mergeItem('@storage_Key', JSON.stringify({
                            accessToken: data.accessToken,
                            refreshToken: data.refreshToken
                        }));
                        return axiosPrivate(prevRequest);
                    } catch (error) {
                        await AsyncStorage.mergeItem('@storage_Key', JSON.stringify({ id: '', accessToken: '', refreshToken: '' }));
                        dispatch(login({ name: '', id: '' }));
                        return Promise.reject(error);
                    };
                };

                return Promise.reject(error);
            }
        );

        return () => {
            axiosPrivate.interceptors.request.eject(requestIntercept);
            axiosPrivate.interceptors.response.eject(responseIntercept);
        };
    }, []);

    return axiosPrivate;
};

Notice that I highlighted the code inside the Request interceptor, to display a console.log with the request address.

As I said above, whenever I need to access the REST Api, I use the hook created above, as in the following line where I am in a component that will display a list of sales:

export default props => {
    const axiosPrivate = useAxiosPrivate();

    const fetchSale = async () => {
            const { data } = await axiosPrivate.get('sales/${dateInitial}/${dateFinal}');
            ...
    };
}

So far so good... The useAxiosPrivate hook is mounted and the console.log is output as follows:

(NOBRIDGE) LOG sales/2025-03-02/2025-03-02

However, if I go to another component to display a list of customers, I will set up another useAxiosPrivate hook. When mounting it will create other interceptors and that's where the strange behavior appears. Only one request is made to the Rest API, as it should be, but the log console is displayed twice.

export default props => {
        const axiosPrivate = useAxiosPrivate();
    
        const fetchClient = async () => {
                const { data } = await axiosPrivate.get('clients');
                ...
        };
    }

(NOBRIDGE) LOG clients
(NOBRIDGE) LOG clients

Then I try to access a product list in another component. To do this I use a new instance of useAxiosPrivate. Again, only one request is made, but now there are 3 console.log requests:

export default props => {
            const axiosPrivate = useAxiosPrivate();
        
            const fetchProduct = async () => {
                    const { data } = await axiosPrivate.get('products');
                    ...
            };
        }

(NOBRIDGE) LOG products
(NOBRIDGE) LOG products
(NOBRIDGE) LOG products

And finally, if I go back to the Sales or Customers list, which each already have a useAxiosPrivate mounted, they start to display 3 console.log, even though they only make one request.

This behavior is strange to me because I imagine that each useAxiosPrivate has only one Request and Response interceptor associated in its assembly.

Why then every time I create a new useAxiosPrivate the console.log in the code is executed once again?


Solution

  • The hooks are all sharing the same instance of axios. Each mounted hook adds an interceptor, so what you're seeing is expected. The interceptors aren't removed (ejected) until that hook unmounts.

    You should likely just move your interceptors out of the hook, especially for something like auth refresh that you'd want on each request anyway. What is the benefit you're looking for by placing them in a hook?