react-router icon indicating copy to clipboard operation
react-router copied to clipboard

[Feature]: allow providing `history` object when calling `createBrowserRouter()`

Open jorroll opened this issue 1 year ago β€’ 3 comments

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

  1. history.listen() only accepts a single listener
  2. If I work around #1 and 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).

jorroll avatar Oct 07 '22 05:10 jorroll

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

ChristophP avatar Oct 08 '22 20:10 ChristophP

Yeah, this is really needed to be able to navigate outside of React components.

Mikilll94 avatar Oct 10 '22 16:10 Mikilll94

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.

sergioavazquez avatar Oct 17 '22 13:10 sergioavazquez

i'm stuck too, please, it is imprescindible

yangoff avatar Nov 01 '22 15:11 yangoff

Yes, please Shopify :-) https://remix.run/blog/remixing-shopify

ChristophP avatar Nov 01 '22 16:11 ChristophP

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

brophdawg11 avatar Nov 02 '22 20:11 brophdawg11

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.

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

yann-combarnous avatar Nov 03 '22 11:11 yann-combarnous

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

brophdawg11 avatar Nov 03 '22 13:11 brophdawg11

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

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.

yann-combarnous avatar Nov 03 '22 14:11 yann-combarnous

Updating to RouterProvider

Please note that you can make the jump to <RouterProvider> really easily:

See this live

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 router to 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

ryanflorence avatar Nov 03 '22 19:11 ryanflorence

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

evoyy avatar Nov 05 '22 03:11 evoyy

updating to router provider

ts reported an error during use image

hsbtr avatar Nov 06 '22 10:11 hsbtr

@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

rtatarinov avatar Nov 07 '22 11:11 rtatarinov

Put it in a module and import it both places, I was just showing the code, not the organization of it :)

ryanflorence avatar Nov 08 '22 03:11 ryanflorence

@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

Hypnosphi avatar Nov 08 '22 20:11 Hypnosphi

So React components probably shouldn't use router at 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 avatar Nov 08 '22 20:11 jorroll

@jorroll nice idea, this should work

Hypnosphi avatar Nov 08 '22 21:11 Hypnosphi