BlogReactJS

React axios interceptor examples (with hooks & context)

Written by Codemzy on May 17th, 2024

Axios works great with React, but if you want to use hooks or context within your interceptors you might find it a little tricky. In this blog post, we'll look at a couple of ways you can get hooks in your interceptors, and create an axios context provider for React.

You don't need to do anything fancy to get axios working in React. You can create an axios.js file, and pretty much all the vanilla JavaScript examples from the axios docs will work.

📄 /utils/axios.js

import axios from 'axios';

// axios instance
const axiosInstance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  withCredentials: true,
});

export { axiosInstance };

You can even add an interceptor...

📄 /utils/axios.js

import axios from 'axios';

// axios instance
const axiosInstance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  withCredentials: true,
});

// Add a response interceptor
axiosInstance.interceptors.response.use(function (response) {
  console.log("You got an axios response!");
  return response;
}, function (error) {
  console.log("You got an axios error!");
  return Promise.reject(error);
});

export { axiosInstance };

But if you want to use hooks or context or some other React-specific features, you'll run into problems. Because those features only work inside React.

I wanted to do this in my response interceptor. I wanted to catch any 401 errors (which tell me the user is no longer authenticated) and log the user out of my app.

The user object was stored in React Context, so I'd need to access that context and call userContext.remove() to set the user back to false and let my app know they need to log back in.

Something like this:

const userContext = useUser();

axiosInstance.interceptors.response.use(function(response) {
  return response;
}, function(error) {
  if (user && error.response.status === 401) {
    userContext.remove(); // sets user back to false
  }
  return Promise.reject(error);
});

Custom interceptor hook for axios

The simplest way I could figure out to achieve what I wanted was to create a custom useAuthInterceptor() hook. It uses the axios instance we created earlier, but we will call it within a React component so we can access context and other React features (like useEffect()).

We'll add the interceptor inside a useEffect() because we only want to add it once (not every time our component renders or some state changes!).

We'll have a dependency [userContext.user] because we need to access the user object to know if a user is logged in on the app. We will return a cleanup function that removes the interceptor axiosInstance.interceptors.response.eject so we don't end up with multiple interceptors just because the user changed.

📄 /hooks/useAuthInterceptor.js

import React from 'react';
import { axiosInstance } from '../utils/axios';
import { useUser } from '../context/UserContext';

// hook for intercepting api requests
export const useAuthInterceptor = function() {
  // get the user context
  const userContext = useUser();
  // auth interceptor (now we have user can check for 401 responses in api calls)
  React.useEffect(() => {
    const authInterceptor = axiosInstance.interceptors.response.use(function(response) {
      return response;
    }, function(error) {
      if (userContext.user && error.response.status === 401) {
        userContext.remove(); // sets user back to false
      }
      return Promise.reject(error);
    });
    return () => {
      axiosInstance.interceptors.response.eject(authInterceptor); // remove interceptor on dismount/auth change
    };
  }, [userContext.user]); // run if user changes
};

Nice, now we can add our custom hook at the router level so it captures all the API requests.

function Routes(props) {
  useAuthInterceptor(); // adds the interceptor

  //...
};

function App() {
  return (
    <ErrorBoundary>
      <UserProvider>
        <Routes />
      </UserProvider>
    </ErrorBoundary>
  )
};

Going full React with AxiosContext

The hook worked great for my use case, but I thought I would take things one step further and imagine a world where I maybe needed to use several interceptors for different things, and maybe I needed to make sure that the interceptor was loaded for network requests during app mounts.

Here's how I would create an AxiosContext.

📄 /context/AxiosContext.js

import axios from 'axios';
import React from 'react';

// axios instance
const axiosInstance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  withCredentials: true,
});

// axios context
const AxiosContext = React.createContext(null);

// axios provider
function AxiosProvider({ instance = axiosInstance, ...props }) {
  return (
    <AxiosContext.Provider value={instance} {...props} />
  );
};

// use the axios context
function useAxios() {
  const instance = React.useContext(AxiosContext);
  if (instance === undefined) {
    throw new Error("useAxios must be used within a AxiosProvider");
  }
  return instance;
};

// hook for intercepting api requests
const useRequestInterceptor = function(config = (c) => c, error = (e) => Promise.reject(e), deps = []) {
  const axiosInstance = useAxios();

  React.useEffect(() => {
    const requestInterceptor = axiosInstance.interceptors.request.use(config, error);
    return () => {
      axiosInstance.interceptors.request.eject(requestInterceptor); // remove if deps change
    };
  }, [...deps]); // run if dependencies change
};

// hook for intercepting api responses
const useResponseInterceptor = function(success = (r) => r, error = (e) => Promise.reject(e), deps = []) {
  const axiosInstance = useAxios();

  React.useEffect(() => {
    const responseInterceptor = axiosInstance.interceptors.response.use(success, error);
    return () => {
      axiosInstance.interceptors.response.eject(responseInterceptor); // remove if deps change
    };
  }, [...deps]); // run if dependencies change
};


export { AxiosProvider, useAxios, useRequestInterceptor, useResponseInterceptor };

That's a fair chunk of code so let me explain it (and how to use it) a little bit...

The AxiosProvider

import axios from 'axios';
import React from 'react';

// axios instance
const axiosInstance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  withCredentials: true,
});

// axios context
const AxiosContext = React.createContext(null);

// axios provider
function AxiosProvider({ instance = axiosInstance, ...props }) {
  return (
    <AxiosContext.Provider value={instance} {...props} />
  );
};

Ok, so I've put my axios instance in here because I only use one instance throughout my app. But if you use multiple instances, you'll notice the AxiosProvider also takes an instance prop so you can override the default one.

The useAxios hook

// use the axios context
function useAxios() {
  const instance = React.useContext(AxiosContext);
  if (instance === undefined) {
    throw new Error("useAxios must be used within a AxiosProvider");
  }
  return instance;
};

The useAxios() hook will get the axios instance from the nearest AxiosProvider. It's the same as the native React.useContext() except that it throws a handy error to tell you if you're calling it outside the context provider.

The axios interceptor hooks

// hook for intercepting api requests
const useRequestInterceptor = function(config = (c) => c, error = (e) => Promise.reject(e), deps = []) {
  const axiosInstance = useAxios();

  React.useEffect(() => {
    const requestInterceptor = axiosInstance.interceptors.request.use(config, error);
    return () => {
      axiosInstance.interceptors.request.eject(requestInterceptor); // remove if deps change
    };
  }, [...deps]); // run if dependencies change
};

// hook for intercepting api responses
const useResponseInterceptor = function(success = (r) => r, error = (e) => Promise.reject(e), deps = []) {
  const axiosInstance = useAxios();

  React.useEffect(() => {
    const responseInterceptor = axiosInstance.interceptors.response.use(success, error);
    return () => {
      axiosInstance.interceptors.response.eject(responseInterceptor); // remove if deps change
    };
  }, [...deps]); // run if dependencies change
};

Ok, these should be pretty familiar to you as they are the framework used to build the useAuthInterceptor() hook earlier. These hooks abstract all the generic stuff, so we can reuse them for any custom interceptor hooks we need.

Our app tree will now look like this:

function App() {
  return (
    <ErrorBoundary>
      <AxiosProvider>
        <UserProvider>
          <Routes />
        </UserProvider>
      </AxiosProvider>
    </ErrorBoundary>
  );
};

And now I can add a custom interceptor for the user 401 errors - this time, I'll add it right in my UserProvider and get the user object from the horse's mouth 🐴.

import React from 'react';
import { useResponseInterceptor } from './AxiosContext';
import Loading from '../components/Loading';

// user provider
function UserProvider(props) {
  const { data: user, ...userQuery } = useGetUser();

  // add a response interceptor to handle 401 errors
  useResponseInterceptor(function(response) {
    return response;
  }, function(error) {
    if (user && error.response.status === 401) {
      remove(); // sets user back to false
    }
    throw(error);
  }, [user]);

  // loading user
  if (userQuery.isLoading) {
    return <Loading />;
  }

  // remove user
  function remove() {
    //...
  };

  return (
    <UserContext.Provider value={user} {...props} />
  );
};

Network calls during render

If you have any network calls that happen when your app loads, the interceptors created in a useEffect() might not have loaded yet. To fix this, you can add a ready state to the hook...

// hook for intercepting api responses
const useResponseInterceptor = function(success = (r) => r, error = (e) => Promise.reject(e), deps = []) {
  const axiosInstance = useAxios();
  const [ready, setReady] = React.useState(false);

  React.useEffect(() => {
    const responseInterceptor = axiosInstance.interceptors.response.use(success, error);
    setReady(true);
    return () => {
      axiosInstance.interceptors.response.eject(responseInterceptor); // remove if deps change
    };
  }, [...deps]); // run if dependencies change

  return ready;
};

And now you can hold off returning the children components until the interceptor has loaded:

import React from 'react';
import { useResponseInterceptor } from './AxiosContext';
import Loading from '../components/Loading';

// user provider
function UserProvider(props) {
  const { data: user, ...userQuery } = useGetUser();

  // add a response interceptor to handle 401 errors
  let interceptorReady = useResponseInterceptor(function(response) {
    return response;
  }, function(error) {
    if (user && error.response.status === 401) {
      remove(); // sets user back to false
    }
    throw(error);
  }, [user]);

  // loading user
  if (userQuery.isLoading || !interceptorReady) {
    return <Loading />;
  }

  // remove user
  function remove() {
    //...
  };

  return (
    <UserContext.Provider value={user} {...props} />
  );
};

You'll only need to add this state if your interceptor needs to be added in a hook (rather than on the initial axios instance) and is needed for network requests on render. You can add multiple interceptors to your axios instance - I found that adding the bulk of my interceptors to the axios instance with vanilla JavaScript (in the original config) and only moving the 401 response interceptor to a hook made this extra state unnecessary.