Dragos Rizescu
Dragos Rizescu

Reputation: 3488

How to Access History Object Outside of a React Component

First of all, I am pretty familiar with the withRouter HoC, however, in this case, it doesn't help because I do not want to access the history object in a component.

I am trying to achieve a mechanism that will redirect the user to the login page if I receive back a 401 from a API endpoint. For making http requests I am using axios. I have around 60 endpoints that I need to cover, that are used in a dozen of components throughout my app.

I want to create a decorator function to the axios instance object, that:

1. makes the request
2. if fail && error_code = 401, update user route to `/login`
3. if success, return promise

The problem I have with the above is to update the route of the user. Previously, in react-router-v3, I could have imported the browserHistory object directly from the react-router package, which is no longer possible.

So, my question is, how can I access the history object outside of the React Component without passing it trough the call stack?

Upvotes: 57

Views: 29006

Answers (7)

Shawn
Shawn

Reputation: 419

I created a solution that could solve this issue. Access react router dom history object outside React component

I think this approach will work with both React-router v4 and v5.

Upvotes: 0

Fotios Tsakiris
Fotios Tsakiris

Reputation: 1556

One simple way is to useHistory() in App.js and then use render and pass history as an attribute of the component:


function App() {
  const history = useHistory();
<Router>
 <Route
 path={nav.multiCategoriesNoTimer}
 render={() => <MultiCategoriesNoTimer history={history} />}
/>
</Router>
}
const MixMultiGameNoTimer = (props: any) => {
if (true) {
    return (
      <NoQuestionsHereScreen history={props.history} />
    );
  }
}
const NoQuestionsHereScreen = (props: any) => {
  return (
    <div className='no-questions-here' >
      <Button
        title="Go back"
        onClick={() => props.history.push(nav.home)}
      />
    </div>
  );
};

There is a bit of drilling, but it works and that for many future versions too>

Upvotes: 0

Muhammed Yasir MT
Muhammed Yasir MT

Reputation: 2014

Here is a solution that worked for me in latest version(5.2.0)

router/index.js

import { BrowserRouter, Switch } from "react-router-dom";
import { Routes } from "./routes";

export const Router = () => {
  return (
    <BrowserRouter>
      <Switch>
        <Routes />
      </Switch>
    </BrowserRouter>
  );
};

router/routes.js

import React, { createRef } from "react";
import { Route, useHistory } from "react-router-dom";
import { PageOne, PageTwo, PageThree } from "../pages";

export const historyRef = createRef();

export const Routes = () => {
  const history = useHistory();
  historyRef.current = history;

  return (
    <>
      <Route exact path="/" component={PageOne} />
      <Route exact path="/route-one" component={PageTwo} />
      <Route exact path="/route-two" component={PageThree} />
    </>
  );
};

And use it as below

historyRef.current.replace("/route-two");

Upvotes: 3

Ayaz Malik
Ayaz Malik

Reputation: 11

I am providing my solution here as accepted answer does not address the new versions of React Router and they require reload of the page to make that solution work.

I have used the same BrowserRouter. I have created a class with static functions and a member history instance.

/*history.js/

class History{
   static historyInstance = null;

   static push(page) {
        History.historyInstance.push(page);
   }

}

/*app-router.js/

const SetHistoryInstance = () => {
   History.historyInstance = useHistory();
   return (null);
};

const AppRouter = () => {
  
  return (
  <BrowserRouter>
    <SetHistoryInstance></SetHistoryInstance>
    <div>
      <Switch>
        <Route path={'/'} component={Home} />
        <Route path={'/data'} component={Data} exact />
      </Switch>
    </div>
  </BrowserRouter>
)};

Now you can import history.js anywhere in your app and use it.

Upvotes: 1

manna
manna

Reputation: 198

Today, I faced the same issue. Maybe my solution helps somebody else.

src/axiosAuthenticated.js

import axios from 'axios';
import { createBrowserHistory } from 'history';


const UNAUTHORIZED = 401;

axios.interceptors.response.use(
  response => response,
  error => {
    const {status} = error.response;
    if (status === UNAUTHORIZED) {
      createBrowserHistory().push('/');
      window.location.reload();
    }
    return Promise.reject(error);
 }
);

export default axios;

Also, if you want to intercept any request to add token stored in LocalStorage:

let user = JSON.parse(localStorage.getItem('user'));

var authToken = "";
if (user && user.token)
  authToken = 'Bearer ' + user.token;

axios.defaults.headers.common = {'Authorization': `${authToken}`}

To use it, instead of importing from 'axios', import from 'axiosAuthenticated' like this:

import axios from 'utils/axiosAuthenticated'

Upvotes: 6

BTMPL
BTMPL

Reputation: 1591

react-router v4 also provides a way to share history via the history package, namely createBrowserHistory() function.

The important part is to make sure that the same history object is shared across your app. To do that you can take advantage of the fact that node modules are singletons.

Create a file called history.js in your project, with the following content:

import { createBrowserHistory } from 'history';

const history = createBrowserHistory();
export default history;

You can then just import it in your application via:

import history from "./history.js";

Please note that only Router accepts the history prop (BrowserRouter does not), so be sure to update your router JSX accordingly:

import { Router } from "react-router-dom";
import history from "./history.js";

// and then in your JSX:
return (
  <Router history={history}>
    {/* routes as usuall */}
  </Router>
)

A working example can be found at https://codesandbox.io/s/owQ8Wrk3

Upvotes: 70

Chaim Friedman
Chaim Friedman

Reputation: 6253

I just encountered this same issue, and following is the solution I used to solve this problem.

I ended up creating a factory function which returns an object that has all my services functions. In order to call this factory function, an object with the following shape must be provided.

interface History {
    push: (location: string) => void;
}

Here is a distilled version of my factory function.

const services = {};

function servicesFactory(history: History) {
    const countries = countriesFactory(history);
    const local = {
        ...countries,
    };
    Object.keys(local).forEach(key => {
        services[key] = local[key];
    });

}

Now the file where this function is defined exports 2 things.

1)This factory function

2)the services object.

This is what the countries service looks like.


function countriesFactory(h: History): CountriesService {
    const countries: CountriesService = {
        getCountries() {
            return request<Countries>({
                method: "get",
                endpoint: "/api/countries",
            }, h)
        }
    }
    return countries;
}

And finally here is what my request function looks like.

function request<T>({ method, endpoint, body }: Request, history: History): Promise<Response<T>> {
    const headers = {
        "token": localStorage.getItem("someToken"),
    };
    const result: Response<T> = {
        data: null,
        error: null,
    };
    return axios({
        url: endpoint,
        method,
        data: body,
        headers,
    }).then(res => {
        result.data = res.data;
        return result;
    }).catch(e => {
        if (e.response.status === 401) {
            localStorage.clear();
            history.push("/login");
            return result;
        } else {
            result.error = e.response.data;
            return result;
        }
    });
}

As you can see the request function exepcts to have the history object passed to it which it will get from the service, and the service will get it from the services factory.

Now the cool part is that I only ever have to call this factory function and pass the history object to it once in the entire app. After that I can simply import the services object and use any method on it without having to worry about passing the history object to it.

Here is the code of where I call the services factory function.

const App = (props: RouteComponentProps) => {
  servicesFactory(props.history);
  return (
    // my app and routes
  );
}

Hope someone else who finds this question will find this useful.

Upvotes: 1

Related Questions