react router v6 navigate outside of components
Asked Answered
A

8

56

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?

Artillery answered 7/11, 2021 at 11:23 Comment(2)
Maybe this would help: reactrouter.com/docs/en/v6/upgrading/…Hymeneal
@ColdAtNight thanks, but it is not specified there how to unage navigate outside of component. And that's exactly what I needArtillery
O
47

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 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.

Notes on RRDv6.4+

If you are using RRDv6.4+ and not using the Data routers the good-ish news is that unstable_HistoryRouter is still being exported through at least RRDv6.7.0. You can follow along the filed issue in the repo here.

If you are using the Data routers then the new "unstable" method is to use an attached navigate function from the router object directly.

Example:

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

const router = createBrowserRouter(...);

...

router.navigate(targetPath, options);
Outgoings answered 17/11, 2021 at 7:20 Comment(13)
What would the CustomRouter look like in typescript?Dick
also, if I'm only interested in replacing <Router history={history}>{children}</Router> then do I need {...props} location={state.location} navigationType={state.action} or do I only pass history?Dick
@Dick react-router-dom is written in Typescript, you could likely lift the types from source. For second question, the code in my answer is the minimum required. The Router requires the location and navigator props, the rest are optional.Outgoings
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.Dick
@Dick 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.Outgoings
Looks like unstable_HistoryRouter has been removed in the latest version :-(Patio
@Patio Yeah, the HistoryRouter was unfortunately removed in RRDv6.4 with the introduction of their new Data APIs and revamp of their contexts. It's going to make using RRD with Redux (i.e. redux-first-history) a little more interesting/difficult/impossible.Outgoings
@DrewReese strange as this seems like a pretty common use-case to me...Patio
@Patio Yeah, quite the bummer if you ask me. On one hand it makes sense RRD is moving towards fetching data when a route loads, but it seems they are moving in on territory redux-toolkit/query already handles and handles quite nicely... at the expense of being able to issue imperative navigation actions from elsewhere in the app outside the router/react code, i.e. asynchronous actions. I don't fully understand the impetus here on RRD maintainers' part.Outgoings
@DrewReese yup plus it doesn't look like they have an official stance on how to achieve this now (unless my google-fu is lacking). kinda looks like they are just washing their hands of the issue :-(Patio
I get a typescript error for window on const history = createBrowserHistory({ window });Homeo
@FiddleFreak window is of type Window. See docs and source.Outgoings
@Patio and others, FWIW the HistoryRouter wasn't actually removed, it was just exported as unstable_HistoryRouter. It still works even through the current/latest version. I updated my answer but forgot to ping you in case it still mattered at all for you.Outgoings
S
8

More simpler code ([email protected]), I did like this

1- create a browser router Object router.tsx :

import { createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
  {
    path: '/',
    element: <HomePage />,
  },
  {
    path: 'page',
    element: <SomePage />,
  }
]);
export default router;

2- Then in the App.tsx (or where you want to use the router):

import {  RouterProvider } from 'react-router-dom';
import router from '@/router';

function App() {
  return (
    <main>
      <RouterProvider router={router} />
    </main>
  );
}

export default App;

3- now to navigate from outside the components

import router from '@/router';
...
router.navigate('/path-to-go');
Skelton answered 15/9, 2023 at 6:47 Comment(1)
But please make sure that it doesn't lead to circular dependencies! Otherwise, you'll shoot yourself in a knee like I didFloreneflorentia
P
6

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")
);
Pricket answered 2/3, 2022 at 11:2 Comment(2)
There error in TypeScriptAlbarran
typescript error Module '"history"' has no exported member 'BrowserHistory'Homeo
L
3

Typescript solution with HistoryRouter from react-router-dom

Different from other answers, this solution uses the HistoryRouter imported from react-router-dom and using TypeScript.

1. Create a new file with your CustomRouter component. I'm placing it at "./components/CustomRouter.tsx" in this example.

import React, { FC, PropsWithChildren } from "react";
import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom";
import { createBrowserHistory } from "history";

const history = createBrowserHistory({ window });

const CustomRouter: FC<PropsWithChildren> = ({ children, ...props }) => {
    return (
    <HistoryRouter history={history} {...props}>
        {children}
    </HistoryRouter>
    );
};

export const rootNavigate = (to: string) => {
    history.push(to);
};

export default CustomRouter;

2. Import the CustomRouter and use it in place of the BrowserRouter

[...]
import CustomRouter from "./components/CustomRouter";
[...]
ReactDOM.render(
    <CustomRouter>
    [...]
    </CustomRouter>,
    root
);

3. Import and use "rootNavigate" anyware to navigate.

import { rootNavigate } from "../components/CustomRouter";

function doAnything() {
    alert("Ok, will do");
    rootNavigate("/anywhere-you-want");
}
Lanfranc answered 19/6, 2022 at 3:40 Comment(2)
Fantastic solution. You saved my day! By the way, I believe that the developers of react router dom are overcomplicating things. A very easy thing ("call a function to navigate to a route") needs a custom router. Seriously? In any library that has the concept of "routes" should have a "navigate(path)" function as a built-in primitive.Popliteal
I get two typescript errors... #1 window on const history = createBrowserHistory({ window }); and #2 Property 'encodeLocation' is missing in type 'History<unknown>' but required in type 'History'Homeo
C
2

Below approach worked for me, using HistoryRouter (React Router v6):

//1.
//create some file like HistoryRouterObject.ts, which will return the history
//object from anywhere
import { createBrowserHistory } from "history";

const historyObject = createBrowserHistory({ window });
export default historyObject;



//2.
//in your index.tsx, wrap your components with HistoryRouter and pass it the 
//historyObject above 
import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom";
import historyObject from './somewhere in your app/HistoryRouterObject'

root.render(
<HistoryRouter history={historyObject}>
      //your components and Routers ...
</HistoryRouter>
);



//3.
//now  you can use the history object in any non-react component function, like IsUserAuthenticated.ts, 

import historyObject from './somewhere in your app/HistoryRouterObject'

function xyz(){
historyObject.replace('/');
//or
historyObject.push("/");
}
Caithness answered 12/7, 2022 at 9:32 Comment(1)
I get two typescript errors... #1 window on const historyObject = createBrowserHistory({ window }); and #2 Property 'encodeLocation' is missing in type 'History<unknown>' but required in type 'History'Homeo
E
2

React Router Dom 6.5

  1. Set up your routes with createBrowserRouter

    import { createBrowserRouter } from "react-router-dom";
    
    const router = createBrowserRouter([
        {
          path: "/",
          element: <Root />,
          children: [
            {
              path: "children",
              element: <Children />,
            },
          ]
        }
     ])
    
  2. import the router and use its navigate property method to redirect with your designated path

     import router from "@router/index";
    
     router.navigate("/auth/login");
    
Eventful answered 21/12, 2022 at 8:48 Comment(3)
This is a big hack using a private api, but it does work.Uncomfortable
why sir BrandonEventful
router.navigator has two types of argument that it receives. As long as you are not providing a number as an argument to router.navigator method, it seems fine to use it.Needham
F
1

React Router 6 has a redirect component method you can use here: https://reactrouter.com/en/main/fetch/redirect

Finalize answered 7/12, 2022 at 17:28 Comment(0)
R
0
  1. Install history:
yarn add history
  1. Create a history.ts (or js) file:
import { createBrowserHistory } from 'history';
export const history = createBrowserHistory();
  1. Replace your BrowserRouter to a HistoryRouter from unstable_HistoryRouter, using your history as argument:
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';
import {history} from './history';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <HistoryRouter history={history}>
       ...
    </HistoryRouter>
  </React.StrictMode>
);
  1. Now you can navigate outside React Components (ts or js files):
import { history } from './history';
...
history.push('/');
Remonstrant answered 20/10, 2022 at 5:3 Comment(1)
I'm getting a typescript error for the history on the left side of the equals. Property 'encodeLocation' is missing in type 'BrowserHistory' but required in type 'History'Homeo

© 2022 - 2024 — McMap. All rights reserved.