15

In react-router v5 i created history object like this:

import { createBrowserHistory } from "history";
export const history = createBrowserHistory();

And then passed it to the Router:

import { Router, Switch, Route, Link } from "react-router-dom";
<Router history={history}>
 ... my routes
</Router>

I did it for the opportunity to usage history outside of component:

   // store action
    logout() {
        this.user = null;
        history.push('/');
    }

This way I moved the logic to the store and the components were kept as clean as possible. But now, in react router v6 i cant do the same. I can still navigate using useNavigate() inside my component, but i cannot create a navigate to use its into my store. Is there any alternative?

Guillaume Racicot
  • 36,309
  • 8
  • 69
  • 115
kofyohugna
  • 197
  • 1
  • 2
  • 6
  • Maybe this would help: https://reactrouter.com/docs/en/v6/upgrading/v5#use-usenavigate-instead-of-usehistory – Bar Nov 07 '21 at 11:38
  • 1
    @ColdAtNight thanks, but it is not specified there how to unage ```navigate``` outside of component. And that's exactly what I need – kofyohugna Nov 07 '21 at 11:53

2 Answers2

18

Well, it turns out you can duplicate the behavior if you implement a custom router that instantiates the history state in the same manner as RRDv6 routers.

Examine the BrowserRouter implementation for example:

export function BrowserRouter({
  basename,
  children,
  window
}: BrowserRouterProps) {
  let historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory({ window });
  }

  let history = historyRef.current;
  let [state, setState] = React.useState({
    action: history.action,
    location: history.location
  });

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

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

Create a CustomRouter that consumes a custom history object and manages the state:

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}
    />
  );
};

This effectively proxies the custom history object into the Router and manages the navigation state.

From here you swap in the CustomRouter with custom history object for the existing Router imported from react-router-dom.

export default function App() {
  return (
    <CustomRouter history={history}>
      <div className="App">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/profile" element={<Profile />} />
        </Routes>
      </div>
    </CustomRouter>
  );
}

Fork of your codesandbox:

Edit react-router-v6-navigate-outside-of-components

Update

react-router-dom@6 now also surfaces a history router.

HistoryRouter

<unstable_HistoryRouter> takes an instance of the history library as prop. This allows you to use that instance in non-React contexts or as a global variable.

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

const history = createBrowserHistory({ window });

ReactDOM.render(
  <HistoryRouter history={history}>
    {/* The rest of your app goes here */}
  </HistoryRouter>,
  root
);

There is this 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.

Drew Reese
  • 103,803
  • 12
  • 69
  • 96
  • What would the CustomRouter look like in typescript? – cldev Jan 24 '22 at 13:19
  • also, if I'm only interested in replacing {children} then do I need {...props} location={state.location} navigationType={state.action} or do I only pass history? – cldev Jan 24 '22 at 13:39
  • 1
    @cldev `react-router-dom` is written in Typescript, you could likely lift the types from [source](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/index.tsx#L126-L162). For second question, the code in my answer is the minimum required. The [Router](https://github.com/remix-run/react-router/blob/main/packages/react-router/index.tsx#L237-L244) requires the `location` and `navigator` props, the rest are optional. – Drew Reese Jan 24 '22 at 17:40
  • That's very useful, thank you. I get an error on navigationType in my return saying: Type 'Action' is not assignable to type 'Action | undefined'. Type '"PUSH"' is not assignable to type 'Action | undefined'.ts(2322) index.d.ts(99, 5): The expected type comes from property 'navigationType' which is declared here on type 'IntrinsicAttributes & RouterProps'. So I'm wondering if I need to add a type? But then location doesn't error. – cldev Jan 25 '22 at 09:52
  • @cldev I'll be honest, I'm minimally versed in Typescript. You'd be much better off asking a new question on SO with appropriate tags. New posts garner more attention than comments on a months old answer. – Drew Reese Jan 25 '22 at 09:56
1

TypeScript solution of accepted answer

history object:

import { createBrowserHistory } from "history";

const customHistory = createBrowserHistory();

export default customHistory;

BrowserRouterProps is a react-router type.

export interface BrowserRouterProps {
  basename?: string;
  children?: React.ReactNode;
  window?: Window;
}

CustomRouter:

import { useLayoutEffect, useState } from "react";
import { BrowserRouterProps, Router } from "react-router-dom";
import { BrowserHistory } from "history";
import customHistory from "./history";
interface Props extends BrowserRouterProps {
  history: BrowserHistory;
}
export const CustomRouter = ({ basename, history, children }: Props) => {
  const [state, setState] = useState({
    action: history.action,
    location: history.location,
  });
  useLayoutEffect(() => history.listen(setState), [history]);
  return (
    <Router
      navigator={customHistory}
      location={state.location}
      navigationType={state.action}
      children={children}
      basename={basename}
    />
  );
};  

use CustomRouter instead BrowserRouter

ReactDOM.render(
  <React.StrictMode>
    <CustomRouter history={customHistory}>
      <App />
    </CustomRouter>
  </React.StrictMode>,
  document.getElementById("root")
);
Okan Karadag
  • 377
  • 2
  • 9