DEV Community

Cover image for React hooks in Axios interceptors
Arian Hamdi
Arian Hamdi

Posted on • Edited on

React hooks in Axios interceptors

As you know, you can not use React hooks in a place other than React component or custom hooks.

In this article, you will learn how to use hooks in the Axios interceptors.

First, create a React project.

yarn create react-app hooks-in-axios-interceptors

// or

npx create-react-app hooks-in-axios-interceptors
Enter fullscreen mode Exit fullscreen mode

Then create an Axios instance with custom configuration.

import axios from 'axios';

const instance = axios.create({
    baseURL: "https://example.com"
})

export default instance;
Enter fullscreen mode Exit fullscreen mode

As we need React component to use the hooks, let's write a component here.

import axios from 'axios';

let instance = axios.create({
    baseURL: "https://example.com"
}) 

function AxiosInterceptor({children}) {
    return children;
}

export default instance;
export {AxiosInterceptor}
Enter fullscreen mode Exit fullscreen mode

Adding an interceptor in a component is a side effect, so we get help from useEffect hook.

Add the interceptors to the Axios instance in the useEffect.

Note : you must remove interceptors in useEffect return statement, because every execution of useEffect, adds a new interceptor to Axios instance.

import axios from 'axios';
import { useEffect } from 'react'

const instance = axios.create({
    baseURL:  "https://example.com"
})

const AxiosInterceptor = ({ children }) => {

    useEffect(() => {

        const resInterceptor = response => {
            return response;
        }

        const errInterceptor = error => {
            return Promise.reject(error);
        }


        const interceptor = instance.interceptors.response.use(resInterceptor, errInterceptor);

        return () => instance.interceptors.response.eject(interceptor);

    }, [])
    return children;
}


export default instance;
export { AxiosInterceptor }
Enter fullscreen mode Exit fullscreen mode

Now, you can import the intended hooks and use them in the interceptors handler.
for instance, if you want to redirect the request which come back with the status code 401, to the login page, you may use the useNavigate hook ( from react-router-dom ) or use the useRouter hook ( from next/router ).

import axios from 'axios';
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'

const instance = axios.create({
    baseURL:  "https://example.com"
})

const AxiosInterceptor = ({ children }) => {

    const navigate = useNavigate();

    useEffect(() => {

        const resInterceptor = response => {
            return response;
        }

        const errInterceptor = error => {

            if (error.response.status === 401) {
                navigate('/login');
            }

            return Promise.reject(error);
        }


        const interceptor = instance.interceptors.response.use(resInterceptor, errInterceptor);

        return () => instance.interceptors.response.eject(interceptor);

    }, [navigate])

    return children;
}


export default instance;
export { AxiosInterceptor }
Enter fullscreen mode Exit fullscreen mode

Note : as we do not want to destroy the SPA behavior of our application, we don't use window.location for redirecting.

Finally, we wrap the app with the Axios interceptor component.

import { AxiosInterceptor } from './axios'
import ThemeProvider from './theme-context'
import AuthProvider from './auth-context'

function App() {
  return (
    <ThemeProvider>
      <AuthProvider>
        <AxiosInterceptor>
          <div>
            App
          </div>
        </AxiosInterceptor >
      </AuthProvider>
    </ThemeProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Note : if you want to access the other context data, the Axios interceptor component must be the child of the context providers.

Codesandbox Example

conclusion

In this way, you can use the React hooks in the Axios interceptors easily.

Top comments (19)

Collapse
 
iamyoki profile image
Yoki

useEffect is asynchronous to children's effect, so request call might be at the same time with interceptor setup and even sooner, in that case the interceptor might not work as we expected.
So the solution is set a state as false initially, and set it to true after interceptor setup complete, then we return the children

function AxiosIntercetor({children}) {
  const [isSet, setIsSet] = useState(false)

  useEffect(()=>{
    ...
    setIsSet(true)
  })

  return isSet && children
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
galih56 profile image
Galih indra

Damn you save my life, I'd been searching this issue for a full day. You are my hero

Collapse
 
devadossedison profile image
Edison Devadoss • Edited
import axios, {
  AxiosError,
  AxiosRequestConfig,
  AxiosRequestHeaders,
  AxiosResponse
} from 'axios';
import environment from '../config/environment';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css';

import { useSelector, useDispatch } from 'react-redux';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';

const toastId: string = 'interceptor';

interface ErrorData {
  errors: string[];
}

const http = axios.create({
  baseURL: environment.baseURL
});

const parseErrorMessage = (errResponse: AxiosResponse<ErrorData>): string => {
  console.log('errResoponse', errResponse);
  return errResponse?.data ? errResponse.data?.errors[0] : '';
};

const AxiosInstance = ({ children }: any) => {
  const navigate = useNavigate();
  const [isSet, setIsSet] = useState(false);
  const token = useSelector((state: any) => state.auth.token);

  useEffect(() => {
    // needs to fix
    const reqInterceptor = (config: any) => {
      if (token) {
        if (config.headers) {
          config.headers['Authorization'] = `Bearer ${token}`;
        }
      }
      return config;
    };

    const resInterceptor = (response: AxiosResponse) => {
      return response.data;
    };

    const errInterceptor = (error: AxiosError) => {
      const message = parseErrorMessage(
        error.response as AxiosResponse<ErrorData>
      );
      if (message) {
        error.message = message;
      }
      const currentPath = window.location.pathname;
      if (error.response?.status === 401 && !currentPath.includes('login')) {
        console.log('in side', currentPath.includes('login'));
        if (!toast.isActive(toastId)) {
          toast.error(message, { toastId });
        }
        setTimeout(() => {
          // Clear token form db
          navigate('/login');
        }, 3000);
      }
      return Promise.reject(error);
    };

    const interceptorReq = http.interceptors.request.use(
      reqInterceptor,
      (error) => {
        console.log('interceptor req error', error);
      }
    );

    const interceptorRes = http.interceptors.response.use(
      resInterceptor,
      errInterceptor
    );

    setIsSet(true);
    return () => {
      http.interceptors.request.eject(interceptorReq);
      http.interceptors.response.eject(interceptorRes);
    };
  }, [isSet, navigate, token]);
  return isSet && children;
};

export default http;
export { AxiosInstance };

Enter fullscreen mode Exit fullscreen mode

This is my code. In the request interceptor is not call all the time. After login, useEffect of the next screen it makes an API call in that call it should set header from redux. But it is not working. Where is the mistake I make?

Collapse
 
clefayomide profile image
clefayomide • Edited

i think a better approach is to prevent the request from running on initial render. this helped me, stackoverflow

Collapse
 
radandevist profile image
Andrianarisoa Daniel

@iamyoki 's answer is workin for me!! And rather simpler to understand.
I indeed tried preventing the request from running on initial render as you suggested but the interceptor stayed unsed for the following renders too.

Collapse
 
saharakmanoo profile image
Saharak

You are my hero, Thank you very much

Collapse
 
mohaymenrafi profile image
mohaymen_rafi

yes, i got the same issue and tried your solution to return isSet && children, however typescirpt does not allow to do that.
How can I fix in that case?

Collapse
 
seyyed_sina profile image
Seyed Sina • Edited

@mohaymenrafi
try to wrap children in empty tag (i.e: or <></>) and it will work

Collapse
 
soongle profile image
SOONG-E

Thanks for this post! what does "Adding an interceptor in a component is a side effect, so we get help from useEffect hook." mean? i followed this post just same. but in my case there is some problem. so i deleted 'return () => instance.interceptors.response.eject(interceptor);' <- this line.
it's works but little weird.. it runs always twice because of and first is always fail, only second one works. once i delete the then it doesn't work.. so! i deleted useEffect() then it works very well. i tried that using only without useEffect(). then interceptor runs 4 times............i don't know why, but i wanna know...

Collapse
 
arianhamdi profile image
Arian Hamdi

Thanks for the comment.
Adding an interceptor in a component is a side effect because we are manipulating something that is out of our scope of component.

you should eject previous interceptor in the clean up function and in the case you didn't, each time the useEffect is executed, one interceptor will be added to interceptors stack and this not the expected behaviour.

And be aware of new behaviour of strict mode in React 18 that execute effect twice in development mode. you can read more about that in this link Adding Reusable State to StrictMode

I created a codesandbox for the post react 18 codesandbox and a react 17 codesandbox

you can find out that the effects run twice in React 18.

Collapse
 
soongle profile image
SOONG-E

Thanks to your explanation, I totally understand! Your writing saved me, and the comments helped me a lot!
I put eject code back because I thought I needed it and It works well! and also your explanation about strict mode in React 18 helped me! it much better! Thank you! 😊

Collapse
 
seda1094 profile image
Seda Hayrapetyan

Thank you ☺️

Collapse
 
aly3n profile image
aly3n

Great post. Thanks for sharing😍

Collapse
 
gagiknav profile image
Gagik

Thanks, this really helped me!
I inspired for this post and created a stand-alone hook for the interceptors here.
I had a lot of providers and I wanted to avoid that.

Collapse
 
mohaymenrafi profile image
mohaymen_rafi

Great post. It really helped me a lot.
One question here, why we are passing children here or what is the reason for making the Interceptor component as a provider?
Answering the question would clear my concept more on interceptors.
Thanks again.

Collapse
 
polly_plantar profile image
Polly Plantar

I didn't know how to use react to make a unified error interception jump for api before seeing your post, thank you so much, it's literally helpful ! (๑•̀ㅂ•́)و✧

Collapse
 
thanksyouall profile image
Ivan

Thank you very much!

Collapse
 
carloscne profile image
Carlos Queiroz

Thanks for sharing!
In my case, using with useEffect does not work. Maybe I could use with a state to control if the useEffect finished it, but I really don't understand why I need useEffect

Collapse
 
shallyoaknorth profile image
shally-oaknorth

Thanks for this post. I also need to write UTC for it. Can you please assist