How to redirect from axios interceptor with react Router V4?
Asked Answered
V

11

45

I want to make a redirection in axios interceptors when receiving a 403 error. But how can I access the history outside React components ?

In Navigating Programatically in React-Router v4, it's in the context of a React Component, but here i'm trying in axios context

axios.interceptors.response.use(function (response) {
    // Do something with response data
    return response;
  }, function (error) {
    // Do something with response error
    if(error.response.status === 403) { console.log("Redirection needed !"); }

    // Trow errr again (may be need for some other catch)
    return Promise.reject(error);
});
Vertievertiginous answered 4/4, 2017 at 14:45 Comment(4)
Try with this code : import {browserHistory} from 'react-router'; then browserHistory.push("/path");Boni
Tried with import {browserHistory} from 'react-router-dom'; then browserHistory.push("/path") and it's not working, it's the V3 way, isn'it ?Vertievertiginous
yes unfortunately this seems to not work for the V4 router...Boni
link to question for react-router-v6 https://mcmap.net/q/374368/-react-router-v6-how-to-use-navigate-redirection-in-axios-interceptor/14665310Violette
R
41

I solved that by accessing my Redux Store from outside the Component tree and sending it my same action from the logout button, since my interceptors are created in a separated file and loaded before any Component is loaded.

So, basically, I did the following:

At index.js file:

//....lots of imports ommited for brevity
import { createStore, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';
import reducers from './reducers';
import { UNAUTH_USER } from './actions/types'; //this is just a constants file for action types.

const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
const store = createStoreWithMiddleware(reducers);

//Here is the guy where I set up the interceptors!
NetworkService.setupInterceptors(store);

//lots of code ommited again...
//Please pay attention to the "RequireAuth" below, we'll talk about it later

ReactDOM.render(
  <Provider store={store}>
      <BrowserRouter>
          <div>
              <Header />
              <main className="plan-container">
                  <Switch>
                      <Route exact path="/" component={Landing} />
                      <Route exact path="/login" component={Login} />
                      <Route exact path="/signup" component={Signup} />
                      <Route exact path="/calendar" component={RequireAuth(Calendar)} />
                      <Route exact path="/profile" component={RequireAuth(Profile)} />
                  </Switch>
              </main>
          </div>
      </BrowserRouter>
  </Provider>
  , document.querySelector('.main-container'));

And at the network-service.js file:

import axios        from 'axios';
import { UNAUTH_USER } from '../actions/types';

export default {
  setupInterceptors: (store) => {

    // Add a response interceptor
    axios.interceptors.response.use(function (response) {
        return response;
    }, function (error) {
        //catches if the session ended!
        if ( error.response.data.token.KEY == 'ERR_EXPIRED_TOKEN') {
            console.log("EXPIRED TOKEN!");
            localStorage.clear();
            store.dispatch({ type: UNAUTH_USER });
        }
        return Promise.reject(error);
    });

  }
};

Last, but not least, I have a HOC (Higher Order Component) that I wrap my protected components where I do the actual redirect when the session is out. That way, when I trigger the action type UNAUTH_USER, it sets my isLogged property at my session reducer to false and therefore this component gets notified and does the redirect for me, at any time.

The file for require-auth.js component:

import React, { Component } from 'react';
import { connect } from 'react-redux';

export default function(ComposedComponent) {

    class RequireAuth extends Component {

        componentWillMount() {
            if(!this.props.session.isLogged) {
                this.props.history.push('/login');
            }
        };

        componentWillUpdate(nextProps) {
            if(!nextProps.session.isLogged) {
                this.props.history.push('/login');
            }
        };

        render() {
            return <ComposedComponent {...this.props} />
        }
    }

    function mapStateToProps(state) {
        return { session: state.session };
    }

    return connect(mapStateToProps)(RequireAuth);
}

Hope that helps!

Residential answered 8/7, 2017 at 3:12 Comment(6)
First off, this is a great solution with the HOC! But, wouldn't you want to check the next updated session prop in componentDidUpdate, not componentWillUpdate?Likeness
@Likeness when I check it in the componentWillUpdate or componentWillMount, I'm actually avoiding the component to re-render if the session got lost. If I dod that in the componentDidUpdate, it would first get rendered, and then check if the session should be there, which is, in my opinion, a breach in security, and a wrong design.Residential
very good point! Thank you for this enlightening piece of info. I'm still new to React and I didn't realize componentWillUpdate has nextProps and nextState as parameters. I thought I was forced to use compomentDidUpdate in order to reference the latest state, hence my ill-informed question. Thanks again!Likeness
I'm having trouble understanding your solution. The interceptors are not added to any axios instance you'd be exporting, so how do you make api requests that would make use of said interceptors? I suppose you're not exposing window.axios. It would seem you'd need to somehow export the axios instance decorated with interceptors, but I can't see where would that beLewan
Hi @MaciejGurban. As far as I know, axios works like a singleton when imported, so as long as you call the interceptor at some point, it gets applied to the whole application, at the same instance of the library.Residential
I am not using redux but using React Context for managing application state. I am not sure if there is a way to call the user context from outside React. any pointers/suggestions ?Leitao
O
27

I solved this task by creating browser history from history (https://github.com/ReactTraining/history) package and passing it into the interceptor function and then calling .push() method from it.

The main file code (part of it):

// app.js
import { createBrowserHistory } from 'history';
import httpService from './api_client/interceptors';

...

const history = createBrowserHistory();
httpService.setupInterceptors(store, history);

Interceptor configuration:

import axios from 'axios';

export default {
  setupInterceptors: (store, history) => {

      axios.interceptors.response.use(response => {
        return response;
      }, error => {

      if (error.response.status === 401) {
        store.dispatch(logoutUser());
      }

      if (error.response.status === 404) {
         history.push('/not-found');
      }

      return Promise.reject(error);
    });
  },
};

Also, you should use Router from react-router (https://github.com/ReactTraining/react-router) and pass the same history object as history param.

// app.js
...
ReactDOM.render(
  <Provider store={store}>
     <Router history={history}>
        ...
     </Router>
  </Provider>
, document.getElementById('#root'))
Ophthalmology answered 13/3, 2018 at 20:50 Comment(4)
How does the code you're using to make API calls look like in this case, i.e. what do you import to make the requests?Lewan
@MaciejGurban I make simple import of axios library and use get, post etc as usualOphthalmology
I see, and how would you then tests these interceptors?Lewan
Thanks mate you saves meNetta
J
5

This seems to work for me

 function (error) {
            var accessDenied = error.toString().indexOf("401");
            if (accessDenied !== -1) {
              console.log('ACCESS DENIED')
              return window.location.href = '/accessdenied'
            }
          });
Jannelle answered 13/4, 2018 at 19:28 Comment(2)
This will reload the tab entirely and start the react from the very beginning instead of manipulating window.historyUnique
I love the simplicity of this solution ... yes you wipe out the history but at the point where you force a login I can't be bothered really with preserving the history ... the other solutions are way too complex.Nora
A
5

This works perfectly.

window.location.href = `${process.env.REACT_APP_BASE_HREF}/login`;
Aretta answered 29/6, 2020 at 16:6 Comment(1)
this works in next js also if you are using it in axios interceptors. But I'm not sure if it is good practice.Goree
W
5

Heres the modified version of the accepted answer that worked for me.

Wrap the App component in index.js using BrowserRouter otherwise the useHistory() hook wont work.

import React from 'react';
...
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter><App /></BrowserRouter>
  </Provider>,
  document.getElementById('root')
);

Create a seperate file instantiating the custom axios instance

import axios from 'axios';

let headers = {};
const baseURL = "http://localhost:8080"
const jwtToken = localStorage.getItem("Authorization");

if (jwtToken) {
    headers.Authorization = 'Bearer ' + jwtToken;
}

const axiosInstance = axios.create({
    baseURL: baseURL,
    headers,
});

export default axiosInstance;

Create another file with the interceptor methods for the custom axios instance created earlier.

import axiosInstance from "./ServerAxios";
import { useHistory } from "react-router-dom";

const baseURL = "http://localhost:8080"

const SetupInterceptors = () => {
    let history = useHistory();
    axiosInstance.interceptors.response.use(function (response) {
        return response;
    }, function (error) {
        var status = error.response.status;
        var resBaseURL = error.response.config.baseURL;
        if (resBaseURL === baseURL && status === 403) {
            localStorage.removeItem("Authorization");
            history.push("/login");
        }
        return Promise.reject(error);
    });
}

export default SetupInterceptors;

Then import it and call the setup method in the App.js file

...
import { createBrowserHistory } from 'history';
import SetupInterceptors from './middleware/NetworkService';
const App = () => {
  const history = createBrowserHistory();
  SetupInterceptors(history);
...

Then whenever you need to use the custom axios instance, import the instantiated file and use it.

import axiosInstance from "../middleware/ServerAxios";
axiosInstance.post(......);
Whitmire answered 18/7, 2021 at 9:26 Comment(3)
import ServerAxios from "../middleware/ServerAxios"; ServerAxios.post(......);' this should correct as axiosInstance() you created on the top right?Stelmach
Yes you're right @aseladaskon. It's been so long. I will edit the answer. Thanks for pointing this out.Whitmire
Hi @suharsha, I did the way you show in code but some issues when calling the axios instance. i have separate index.ts file with all the async call to back-end node js server. call to that axios interface inside my async methods but it's not calling to that axios.interceptors which is in separate ts file. In there i have that return response.data. I also added to my nextjs _app.tsx file that interceptors file.Stelmach
U
3

Just realized that the question is for react router v4 and I already wrote the answer I used in v5.

I solved this by passing useHistory() from inside a <Router> to axios interceptors.

App.js:

// app.js

function App() {
  return (
    <Router>
      <InjectAxiosInterceptors />

      <Route ... />
      <Route ... />
    </Router>
  )
}

InjectAxiosInterceptors.js:

import { useEffect } from "react"
import { useHistory } from "react-router-dom"
import { setupInterceptors } from "./plugins/http"

function InjectAxiosInterceptors () {
  const history = useHistory()

  useEffect(() => {
    console.log('this effect is called once')
    setupInterceptors(history)
  }, [history])

  // not rendering anything
  return null
}

plugins/http.js:

import axios from "axios";

const http = axios.create({
  baseURL: 'https://url'
})

/**
 * @param {import('history').History} history - from useHistory() hook
 */
export const setupInterceptors = history => {
  http.interceptors.response.use(res => {
    // success
    return res
  }, err => {
    const { status } = err.response
  
    if (status === 401) {
      // here we have access of the useHistory() from current Router
      history.push('/login')
    }
  
    return Promise.reject(err)
  })
}

export default http
Unique answered 9/7, 2021 at 9:44 Comment(1)
How to use the interceptor inside a component.Gemination
V
1

The best solution I found is to define axios.interceptors inside my main React components and use that to handle errors : ( And with withRouter from Router V4 )

import {withRouter} from 'react-router-dom';

class Homepage extends Component {
  static propTypes = {
    history: PropTypes.object.isRequired
  }

  constructor(props){
    super(props);

    let that = this;
    axios.interceptors.response.use(function (response) {
        // Do something with response data
        return response;
      }, function (error) {
        // Do something with response error
        if(error.response.status === 403) { that.handle403() }

        // Trow errr again (may be need for some other catch)
        return Promise.reject(error);
    });

  }

  handle403(){
    this.props.history.push('/login');
  }
Vertievertiginous answered 7/4, 2017 at 11:40 Comment(1)
This relies on component initialization order and won't work if some component makes axios request before the interceptor is installed.Trumpeter
B
1

The accepted answer doesnt solve my problem. After spending time in axios and tickets around interceptor not triggering, i found, axios doesnt support decorating interceptor globally like it is described above. for future readers, please keep in mind that, axios has tagged this global interceptor as feature. so maybe we will get it in the future realse. for ref: https://github.com/axios/axios/issues/993.

I do have a single axios instance for all the api call, so i solved defining interceptor in it.

Boxwood answered 14/1, 2019 at 4:14 Comment(0)
P
1

UPDATE:- useHistory() is deprecated as of now. Use :-

import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
navigate('/login')
Palpitate answered 21/2, 2023 at 7:55 Comment(0)
E
0

I used this approach, very simple and works like a charm:


const setupInterceptors = (navigateTo) => {

    axios.interceptors.response.use(function (response) {
        return response;
    }, function (error) {

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

        return Promise.reject(error);
    });
}


export default function HomePage() {

/* Use navigateTo inside your component */
    const navigateTo = useNavigate();
    setupInterceptors(navigateTo);

    return <div></div>;
}
Elated answered 2/10, 2022 at 0:17 Comment(0)
L
-4

I am using react-router-dom and it has "history" props which can be used in transition to new route

 history.push('/newRoute')
Lewanna answered 1/11, 2017 at 7:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.