React router v6 how to use `navigate` redirection in axios interceptor
Asked Answered
M

7

28
import axios from "axios";
import { useNavigate } from "react-router-dom";

export const api = axios.create({
  baseURL: "http://127.0.0.1:8000/",
  headers: {
    "content-type": "application/json",
  },
});

api.interceptors.response.use(
  function (response) {
    return response;
  },
  function (er) {
    if (axios.isAxiosError(er)) {
      if (er.response) {
        if (er.response.status == 401) {

          // Won't work
          useNavigate()("/login");

        }
      }
    }

    return Promise.reject(er);
  }
);
Mornings answered 13/11, 2021 at 10:26 Comment(1)
Yeah, RRDv6 doesn't expose out the history object now, and the useNavigate is a React hook only validly used in React function components and custom hooks. What type of router is your app using? You'll need to do something similar to how history is/was accessed in RRDv4/5.Manoff
M
30

In the pre-RRDv6 world you would create a custom history object, to be exported and imported and passed to a Router, and imported and accessible in external javascript logic, like redux-thunks, axios utilities, etc.

To replicate this in RRDv6 you need to also create a custom router component so it can be passed an external history object. This is because all the higher level RRDv6 routers maintain their own internal history contexts, so we need to duplicate the history instantiation and state part and pass in the props to fit the base Router component's new API.

import { Router } from "react-router-dom";

const CustomRouter = ({ history, ...props }) => {
  const [state, setState] = useState({
    action: history.action,
    location: history.location
  });

  useLayoutEffect(() => history.listen(setState), [history]);

  return (
    <Router
      {...props}
      location={state.location}
      navigationType={state.action}
      navigator={history}
    />
  );
};

Create the history object you need:

import { createBrowserHistory } from "history";

const history = createBrowserHistory();

export default history;

Import and pass to the new CustomRouter:

import customHistory from '../path/to/history';

...

<CustomRouter history={customHistory}>
  ... app code ...
</CustomRouter>

Import and consume in your axios functions:

import axios from "axios";
import history from '../path/to/history';

export const api = axios.create({
  baseURL: "http://127.0.0.1:8000/",
  headers: {
    "content-type": "application/json",
  },
});

api.interceptors.response.use(
  function (response) {
    return response;
  },
  function (er) {
    if (axios.isAxiosError(er)) {
      if (er.response) {
        if (er.response.status == 401) {
          history.replace("/login"); // <-- navigate
        }
      }
    }

    return Promise.reject(er);
  }
);

Update

react-router-dom exports a history router, i.e. unstable_HistoryRouter.

Example:

import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom";
import history from '../path/to/history';

...

<HistoryRouter history={customHistory}>
  ... app code ...
</HistoryRouter>

Note:

This API is currently prefixed as unstable_ because you may unintentionally add two versions of the history library to your app, the one you have added to your package.json and whatever version React Router uses internally. If it is allowed by your tooling, it's recommended to not add history as a direct dependency and instead rely on the nested dependency from the react-router package. Once we have a mechanism to detect mis-matched versions, this API will remove its unstable_ prefix.

Manoff answered 17/11, 2021 at 22:0 Comment(9)
unstable_HistoryRouter seems to have been removed. The link doesn't work.Tempest
@Tempest The HistoryRouter is still exported. Here's the source, current to v6.6.1. They did remove it from the official documentation. No telling how long it will still be exported though. I updated the link to point to the source code for now.Manoff
In the useLayoutEffect dependencies, in order to avoid errors with a dependency on the object, it would be better to use [JSON.stringify(history)] as the deps array.Shelf
@MattLachman Object references are typically fine as React hook dependencies. Do you have a specific edge-case error in mind that would require JSON serializing the history object like that? If you inspect the react-router source code this is the way they use the hook and history object and don't appear to have any issues.Manoff
I've run into issues where changing an object property doesn't trigger a useEffect hook if the useEffect hook depends on an object or array. That's because the reference didn't change, but the data in it did.Shelf
I forget the specific issue I ran into, but when I came across this answer it helped: https://mcmap.net/q/203711/-passing-array-to-useeffect-dependency-listShelf
@MattLachman From what I've seen the stringification trick is mostly for arrays. That said, if you are React-ing correctly you should be creating new array/object references when a nested property or array element is also updated to be a new reference. In the scenario above though the history object really shouldn't ever change during the life of the app. I'd say it'd be extremely odd, and rare, if it did.Manoff
@DrewReese would i lose support for data API's doing it this way, beause i'm planning to use createBrowserRouter keeping it inside component as i want to have conditional routing by accessing data from apiJamille
@ShamseerAhammed Yes, this solution is incompatible with React-Router@6 Data APIs and Routers. If you are using axios in your route loaders you can simply return/throw a redirect.Manoff
B
8

I had the same issue this is what I did and it worked for me ( based on an answer from a similar question for react router v4

I added a interceptor component inside of the react router config which passes a prop of the useNavigate hook

remove the interceptors from the axios config file and put them in a separate interceptors file

SetupInterceptor.js

//place all imports here

const SetupInterceptors = (navigate) => {
    axios.interceptors.response.use(
       function (response) {
           // Do something with response data
           return response;
       },
       function (error) {
           // Do something with response error
           if (error.response) {
                if (error.response.status === 401 || error.response.status === 403) {
                    navigate('/login');
    
                }
           }
           return Promise.reject(error);
      }
   );
};

export default SetupInterceptors;

axiosconfigfile.js

import axios from "axios";


//axios configuration here

export default axios;

add the AxiosInterceptorComponent to app.js

app.js

import SetupInterceptors from "SetupInterceptor";
import { useState } from "react";

function NavigateFunctionComponent(props) {
    let navigate = useNavigate();
    const [ran,setRan] = useState(false);

    {/* only run setup once */}
    if(!ran){
       SetupInterceptors(navigate);
       setRan(true);
    }
    return <></>;
}


function App(props) {
   return (
       <BrowserRouter>
           {<NavigateFunctionComponent />}
           <Routes>
              {/* other routes here */}
              <Route path="/login" element={<Login/>}></Route>
              {/* other routes here */}
           </Routes>
       </BrowserRouter>
   );
}
Blissful answered 17/12, 2021 at 19:20 Comment(2)
Thanks for the solution, the only issue is that the interceptor will get initialised multiple times on re-renders, meaning multiple similar error handlings for 1 response. To counter this you can set a boolean to check if SetupInterceptors is already calledKalil
okay thank you will update code to fix that issueBlissful
B
5

As of v6.1.0, you can easily redirect outside of react components with ease.

import {createBrowserHistory} from 'history';
import {unstable_HistoryRouter as HistoryRouter} from 'react-router-dom';

let history = createBrowserHistory();

function App() {
  return (
    <HistoryRouter history={history}>
      // The rest of your app
    </HistoryRouter>
  );
}

// Usage
history.replace('/foo');

Just replace BrowserRouter with HistoryRouter and you're good.

Sauce: https://github.com/remix-run/react-router/issues/8264#issuecomment-991271554

Bostow answered 27/1, 2022 at 11:17 Comment(1)
Comment from React Router's Source: It's important to note that using your own history object is highly discouraged. Will this be supported in the future?Katey
P
3

I created a custom hook like this

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

const useAxios = () => {
  const navigate = useNavigate();

  const instance = axios.create({
    baseURL: `${import.meta.env.VITE_APP_BASE_URL}/api/v1/`,
  });

  instance.interceptors.request.use(
    async (config) => {
      // handle config eg-: setting token
      return config;
    },
    (error) => Promise.reject(error),
  );

  instance.interceptors.response.use(
    (response) => response,
    (error: AxiosError) => {
      if (error.response?.status === 401) {
        navigate('/login');
      }
      return Promise.reject(error);
    },
  );

  return { instance };
};

export default useAxios;

Then I can use axios instance when I want

const { instance } = useAxios();

const { data } = await instance.get(`user`);
Pronucleus answered 18/6, 2023 at 12:18 Comment(0)
P
2

Since version 6.1.0 React Router has an unstable_HistoryRouter. With it history can now be used outside of React context again. It is used like this:

// lib/history.js
import { createBrowserHistory } from "history";
export default createBrowserHistory();

// App.jsx
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';
import history from "lib/history";

function App() {
  return (
    <HistoryRouter history={history}>
      // The rest of your app
    </HistoryRouter>
  );
}

// lib/axios.js
import history from "lib/history";
import storage from "utils/storage";

export const axios = Axios.create({})

axios.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (error) => {
    if (error.response?.status === 401) {
      storage.clearToken();
      history.push("/auth/login");
      return Promise.resolve();
    }

    return Promise.reject(error);
  }
);

Source

Asked what the unstable_-prefix means, one of the maintainers answered:

The unstable_ prefix just signifies you could encounter bugs due to mismatched versions of history from what react-router requests itself. While the practical implications are probably going to just create type issues (different types between versions of history), there could also be API or behavior changes that cause more subtle bugs (such as how URLs are parsed, or how paths are generated).

As long as you're keeping an eye on the version of history requested by react-router and ensuring that's in sync with the version used by your app, you should be free of issues. We don't yet have protections or warnings against the mismatch, hence we're calling it unstable for now.

Photodynamics answered 11/1, 2022 at 16:22 Comment(0)
S
2

I have solved this problem by creating a component for the interceptor:

import { useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { axiosInstance } from '../custom-axios';

export const ResponseInterceptor = () => {
  const navigate = useNavigate()


  const interceptorId = useRef<number | null>(null);

  useEffect(() => {
    interceptorId.current = axiosInstance.interceptors.response.use(undefined, (error) => {
      switch (error.response.status) {
        case 401:
          navigate('/login');
          break;
      }
    });

    return () => {
      axiosInstance.interceptors.response.eject(interceptorId.current as number);
    };
  }, [navigate]);

  return null;
};

And connected it to the App component:

const App = () => {
  return (
    <AuthProvider>
      <APIErrorProvider>
        <AppRoutes />
        <ResponseInterceptor />
        <APIErrorNotification />
      </APIErrorProvider>
    </AuthProvider>
  );
};
export default App;

Scholastic answered 9/2, 2022 at 9:40 Comment(2)
nice, i repurposed your solution to instead target the state store i have in my application and it worked! /** * solution taken from https://stackoverflow.com/a/71047161/27698 */ import React from 'react'; import { useAuthStore } from "./appState"; export default function(){ const { setLoggedOut } = useAuthStore(); React.useEffect(() => { axios.interceptors.response.use(undefined, function(error){ switch(error.response.status){ case 401: setLoggedOut(true); break; } }); },[]); return undefined; }Venomous
all good, but your effect is missing navigate dependency. Also should reset interceptor id ref on effect cleanup and instead of casting interceptor id ref to number, better check that it is not nullAcanthous
N
0

The reason this is not working is because you can only consume hooks inside a React component or a custom hook.

Since this is neither, hence the useNavigate() hook is failing.

My advice would be to wrap the entire logic inside a custom hook.

Pseudo code :

import { useCallback, useEffect, useState } from "react"

export default function useAsync(callback, dependencies = []) {
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState()
  const [value, setValue] = useState()

  // Simply manage 3 different states and update them as per the results of a Promise's resolution
  // Here, we define a callback
  const callbackMemoized = useCallback(() => {
    setLoading(true)
    setError(undefined)
    setValue(undefined)
    callback()
      // ON SUCCESS -> Set the data from promise as "value"  
      .then(setValue)
      // ON FAILURE -> Set the err from promise as "error"  
      .catch((error)=> {
          setError(error)
          useNavigate()('/login')
       })
      // Irresp of fail or success, loading must stop after promise has ran
      .finally(() => setLoading(false))
      // This function runs everytime some dependency changes
  }, dependencies)

  // To run the callback function each time it itself changes i.e. when its dependencies change
  useEffect(() => {
    callbackMemoized()
  }, [callbackMemoized])

  return { loading, error, value }
}

Here, just pass your axios call as a callback to the hook.

For reference for migration from v5 to v6 of react router, this video is helpful

Niemi answered 17/11, 2021 at 2:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.