react-router
react-router copied to clipboard
[Feature]: allow providing `history` object when calling `createBrowserRouter()`
What is the new or updated feature that you are suggesting?
The new createBrowserRouter() API internally calls createBrowserHistory() but doesn't provide a way for end-users to get access to the created history object if we'd like to subscribe to history events. I'd like the ability to instantiate createBrowserHistory() myself and provide the history object as an option to createBrowserRouter(). Using the old, v6.3 api, doing something like this was possible via the unstable_HistoryRouter.
It seems likely that something along these lines is already planned, but there doesn't appear to be a tracking issue for this feature.
I actually tried using the internal API myself to implement a custom createBrowserRouter() and provide my own createBrowserHistory() to it (using the new createBrowserHistory export from "@remix-run/router", but
history.listen()only accepts a single listener- If I work around
#1and monkeypatch the history object to accept multiple listeners, it doesn't appear as though the listener is ever actually called (perhaps this is a placeholder method which isn't fleshed out yet? Obviously this is internal API at the moment so I'm not surprised things aren't working yet).
Why should this feature be included?
I'd like to subscribe to history events and respond to them within my application--both before and after react router receives these events (e.g. maybe I want to cancel an event before it reaches React Router).
I'm have a similar need to listen to history changes for other parts of the app on the same page that aren't written in React. Related https://github.com/remix-run/react-router/issues/9385
Yeah, this is really needed to be able to navigate outside of React components.
Being able to navigate outside React components is a key feature. Please add this back as I'm stuck on v6.3 on several projects until you do.
i'm stuck too, please, it is imprescindible
Yes, please Shopify :-) https://remix.run/blog/remixing-shopify
Hey folks! There's a bunch of separate issues around navigating from outside the react tree in 6.4, so I choose this one as the de-facto "source of truth" issue π. I'll drop an answer here and then link off to it from as many other issues as I can find. Let's try to centralize further discussion here if we can so we don't miss anything!
There's some nuance here based on the version you're using so I'll try my best to provide a clear explanation of the paths forward.
Folks on 6.0 through 6.3
For those of you using unstable_HistoryRouter in 6.0->6.3, that was dropped from the updated docs but it's still there and you can still use it (with the same warnings as before). It's not considered stable and is open to breaking in the future.
Folks on 6.4+ but not using a Data Router
If you've upgraded to 6.4+ but aren't using a data router, then you can also still use unstable_HistoryRouter but there are some nuanced changes to the way we interface with window.history internally. Your best bet is probably just to add history@5 as your own dependency and import createBrowserHistory from there.
You can try using the createBrowserHistory from [email protected] but you will need to instantiate it as createBrowserHistory({ v5Compat: true }) to opt-into the v5 behavior. The newer version also currently doesn't allow multiple listeners since it wasn't intended for external consumption, so it's only really useful for navigating from outside the react tree, but not listening for updates from outside the tree. If you need that then go with history@5.
Folks on 6.4+ using a Data Router (RouterProvider)
π Congrats on upgrading to the new data routers and we hope you're loving the UX improvements you can get from loaders/actions/fetchers! If you're still in need of a way to navigate or respond to updates from outside the react tree, then there's both good and bad news!
The good news is that you can just do it manually via the router instance you get from createBrowserRouter! By introducing data APIs, history is really just an implementation detail now and the router is the entry point. This is because we have to fetch data prior to routing so we can't just respond to history events anymore.
let router = createBrowserRouter(...);
// If you need to navigate externally, instead of history.push you can do:
router.navigate('/path');
// And instead of history.replace you can do:
router.navigate('/path', { replace: true });
// And instead of history.listen you can:
router.subscribe((state) => console.log('new state', state));
Now, for the bad news π . Just like unstable_HistoryRouter we also consider this type of external navigation and subscribing to be unstable, which is why we haven't documented this and why we've marked all the router APIs as @internal PRIVATE - DO NOT USE in JSDoc/Typescript. This isn't to say that they'll forever be unstable, but since it's not the normally expected usage of the router, we're still making sure that this type of external-navigation doesn't introduce problems (and we're fairly confident it doesn't with the introduction of useSyncExternalStore in react 18!)
If this type of navigation is necessary for your app and you need a replacement for unstable_HistoryRouter when using RouterProvider then we encourage you use the router.navigate and router.subscribe methods and help us beta test the approach! Please feel free to open new GH issues if you run into any using that approach and we'll use them to help us make the call on moving that towards future stable release.
Thanks folks!
Also w.r.t. this specific issue I'm going to close it as I think the router provides the behavior you need, however please note that you cannot cancel these events - subscribe will be called after the navigations.
I'd like to subscribe to history events and respond to them within my application--both before and after react router receives these events (e.g. maybe I want to cancel an event before it reaches React Router).
If you're looking to cancel an event then that's a different thing - please check out this comment and follow along in that issue - but I don't think we'll be bringing blocking back into v6 since it's always been a bit of a hacky implementation (since you cannot "block" a back/forward navigation from the browser).
Folks on 6.4+ but not using a Data Router
If you've upgraded to 6.4+ but aren't using a data router, then you can also still use
unstable_HistoryRouterbut there are some nuanced changes to the way we interface withwindow.historyinternally. Your best bet is probably just to addhistory@5as your own dependency and importcreateBrowserHistoryfrom there.
@brophdawg11, thx for the explanation. I tried this approach, and I am getting typescript error:
Property 'encodeLocation' is missing in type 'BrowserHistory' but required in type 'History'.ts(2741)
So it seems like this approach is broken due to the new "encodeLocation" requirement for History.
Ah - thanks for calling this out - that's a new 6.4-only thing. Does everything work fine if you ignore that with // @ts-expect-error? That encodeLocation method is new in the internal history and only used in <RouterProvider> so it shouldn't ever be referenced if you're using unstable_HistoryRouter
Ah - thanks for calling this out - that's a new 6.4-only thing. Does everything work fine if you ignore that with
// @ts-expect-error? ThatencodeLocationmethod is new in the internalhistoryand only used in<RouterProvider>so it shouldn't ever be referenced if you're usingunstable_HistoryRouter
Thanks @brophdawg11 , yes silencing TS error works, but may miss future issues. What about adding the new method to "history" package as well and bump to 5.4.0 ? This will be smoother for transition, IMO.
Updating to RouterProvider
Please note that you can make the jump to <RouterProvider> really easily:
Your code probably looks something like this today:
const history = createBrowserHistory({ window });
<unstable_HistoryRouter history={history} />
We're just gonna swap that for createBrowserRouter with a single splat route and <RouterProvider>.
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { createBrowserRouter, RouterProvider } from "react-router-dom"
const router = createBrowserRouter([
// match everything with "*"
{ path: "*", element: <App /> }
])
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
)
And then the rest of your routes will work as they always did inside of components:
import { Routes, Route, Link } from "react-router-dom";
export default function App() {
return (
<main>
<h1>Can use descendant Routes as before</h1>
<ul>
<li>
<Link to="about">About</Link>
</li>
</ul>
<Routes>
<Route index element={<div>Index</div>} />
<Route path="about" element={<div>About Page</div>} />
<Route path="contact" element={<div>Contact Page</div>} />
</Routes>
</main>
)
}
- use the
routerto navigate outside of the tree - if you want to take advantage of the data APIs, incrementally move routes to the router instance when you want to
It is worth mentioning that after updating to <RouterProvider>, the router will catch any errors and if you want them to propagate up to a root error boundary at the top of the component tree, you have to specify an errorElement for every <Route> and re-throw the error:
<Route path="about" element={<div>About Page</div>} errorElement={<RouteErrorBoundary />} />
// ...
function RouteErrorBoundary() {
const error = useRouteError();
throw error;
};
updating to router provider
ts reported an error during use

@ryanflorence Thanks for your suggestion. But actually this approach has a problem withcircular dependency.
The router uses the App, component. But the App component uses any component, which can use the router
Put it in a module and import it both places, I was just showing the code, not the organization of it :)
@ryanflorence unfortunately it won't help because the cycle exists between entities, not just between files. So React components probably shouldn't use routerΒ at all, they have access to useLocation/useNavigate instead
So React components probably shouldn't use
routerat all
@Hypnosphi admittedly I haven't tried any of the suggestions mentioned here yet, but, worst case scenario, couldn't you just use react context to pass the router instance down to components?
@jorroll nice idea, this should work
This may be frowned upon, but I store the router as a property of a global object called $app that I attach to window.
Some form of history listener, synthetic or otherwise, would allow for an analog for forward/backward navigation when using navigate(). navigate can be used within an event handler, to cause forward motion, but if a user requests backward motion, we have no event handler to track this. We end up having to use an effect on Location, which has different lifecycle considerations.
@ryanflorence Thanks for your suggestion. But actually this approach has a problem with
circular dependency. Therouteruses theApp, component. But theAppcomponent uses any component, which can use therouter
When a function uses something that creates a circular dependency, I'll typically inject the dependency into a variable at the top level of the module, so that when the function is finally called, that dependency is available:
Component.jsx:
let router = null;
export function injectRouter(newRouter) {
router = newRouter;
}
export default function Component() {
// use router with impunity, because it was injected as part of the module declaration below.
}
router.js:
import { injectRouter } from 'Component';
export const router = ...
injectRouter(router);
@Valar103769 What's your point? If you need to navigate outside components in a single case, then it's required... Of course most of the navigation occurs inside components. That's not the point. Please don't minimize this issue as it's causing huge problems for a lot of people. Using unstable methods is a workaround, we need a fix.
Hey!
I wonder what would be the suggested approach for micro frontends. Ideally, they would all share the same history. Before, we could create a browser history on the parent app using createBrowserHistory and inject that history on subapps when those are mounted.
Now, we can't pass history as props or even extract it from Router or any of the Router components. I noticed you can pass a Navigator prop to Router, but I couldn't find a way to get that object anywhere, there are no hooks for it. This would also require creating an interface so it could be used by other apps that don't have ReactRouter v6 (or other frameworks even), making it much less flexible than having the history.
Any ideas or examples on how to do it?
@davidpn11 Couldn't that be done with the new router instead of history as explained in this comment https://github.com/remix-run/react-router/issues/9422#issuecomment-1301182219 ?
@ChristophP my biggest concern with passing the router solution is it doesn't allow us to specify nested routes in the micro frontends
for example, it's common to have a shell application that mounts micro frontend applications, where the shell will let the micro frontend define its own nested routes
is there a way to dynamically re-configure the routes in the router ?
also, the shared history object was a way to keep the state of the router in sync across the shell application and the micro frontend applications
for example, each micro frontend would create its own unstable_HistoryRouter, and pass the shared history object to it as a way to have one common history state
@ryanflorence I have a problem referencing the router outside the tree following this example https://github.com/remix-run/react-router/issues/9422#issuecomment-1302564759
I am importing css and css modules in my jsx, and this breaks my application, since I am importing the router in a ts file. The router references <App>, and with it all the css modules which get misinterpreted as malformed typescript
the error is this
body {
^
SyntaxError: Unexpected token '{'
Do you have any suggestions? Thanks for your time and patence
@fdrcslv I don't think that this is a router-specific problem. Where are you importing these? In a test? E.g. for jest you can tell jest to treat these as empty objects: https://stackoverflow.com/questions/39418555/syntaxerror-with-jest-and-react-and-importing-css-files
@dbartholomae I'm importing the router in a ts file where is defined a state machine with which I control part of the redux state and some side effects.
The error only comes up when I import the router there.
Maybe the problem is that the state machine (or rather its parent) is referenced in a component? I would expect a circular reference error, and not this weird css thing.
EDIT:
I tried importing directly the <App> component in my ts file and the same error happens. Since the router references the same <App> I don't know how to get around this issue
@brophdawg11 Are we any closer to a solution? Almost 6 months went by, this feature request is now closed, and as far as I'm aware the only way to navigate outside components is by using an unstable method. Why is this feature request closed? Was there a decision to drop this feature? Thanks!
The recommendation is still the same - router.navigate() is the correct way to trigger a navigation outside components when using RouterProvider. This issue remains closed as we are not planning on exposing history or allowing a custom history because history is no longer the driving "navigator" in a RouterProvider, the router is.
There's been some comments about micro-frontends in here, that is something we are planning on tackling eventually so you can dynamically add new sections of the route tree to your router. For the time being through, remember that a RouterProvider can still use descendant <Routes> (as shown by Ryan in this comment), they just can't participate in data loading APIs (loader/action/fetcher/useMatches/etc.). That should allow you to dynamically render sub-apps via <Routes>: https://codesandbox.io/s/routerprovider-mfe-zj2xq0