-
-
Notifications
You must be signed in to change notification settings - Fork 10.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature]: allow providing history
object when calling createBrowserRouter()
#9422
Comments
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. |
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as off-topic.
This comment was marked as off-topic.
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 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 You can try using the Folks on 6.4+ using a Data Router ( 🎉 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 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 If this type of navigation is necessary for your app and you need a replacement for 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.
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, thx for the explanation. I tried this approach, and I am getting typescript error:
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 |
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 RouterProviderPlease note that you can make the jump to Your code probably looks something like this today: const history = createBrowserHistory({ window });
<unstable_HistoryRouter history={history} /> We're just gonna swap that for 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>
)
}
|
It is worth mentioning that after updating to <Route path="about" element={<div>About Page</div>} errorElement={<RouteErrorBoundary />} />
// ...
function RouteErrorBoundary() {
const error = useRouteError();
throw error;
}; |
@ryanflorence Thanks for your suggestion. But actually this approach has a problem with |
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 |
@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 |
@jorroll nice idea, this should work |
This may be frowned upon, but I store the |
Some form of history listener, synthetic or otherwise, would allow for an analog for forward/backward navigation when using |
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 Now, we can't pass Any ideas or examples on how to do it? |
The recommendation is still the same - 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 |
By taking this stance and not exposing a useful When we want a page to work like it's on the server, everything seems to work. When we want a page to work like it's on the client... it seems like that should work, too, no? Forget how to navigate. How do we add event listeners to |
I'm still confused how testing should work in this case. Currently, we do the following: function createTestWrapper({ history }) {
const store = createReduxStore({history});
return <Provider store={store}><HistoryRouter history={history}>{children}</HistoryRouter></Provider>
}
it('navigates to /page after 5 seconds on click', async () => {
const history = createMemoryHistory({ initialEntries: '/profile' });
render(<Profile />, { wrapper: createTestWrapper({ history}) });
await click(screen.findByText("Navigate in 5 seconds"));
jest.advanceTimersByTime(5_000);
expect(history.location.pathname).toBe('/page');
}) Without a |
@brophdawg11 I understand, but as you wrote above, this is in beta and not well documented. This is not a solution yet. I'm reluctant to refactor my entire router to a beta or unstable approach. What happens if you decide to deprecate this in the future? I understand the complexities that arise from implementing new features, but navigating outside the router is a MUST for many projects. My two cents on this matter, and I'm sure I'm not alone:
The React team did an entire version (React 17) in order not to break compatibility. Which should speak of its importance. Please considering providing a long term, well documented, stable alternative to this issue. Thanks! |
I disagree. Keep in mind that there are two things at play here --
The standard browser API is
That's perfectly fine - you're welcome to wait until we stabilize
I'm not sure how this applies. The Data APIs were introduced in a fully backwards compatible manner and you are not obligated to use them (see above). If you're talking about navigating outside the components then the "solid alternative" is |
We're in disagreement about what a solid alternative is. A I also disagree with you blaming users for not helping you test a beta feature. Not everyone can do that, it depends on team size, infrastructure, etc. Whenever you release a feature, people start depending on it and you have a responsibility to provide a STABLE path forward for it. Otherwise you are at fault. You're leaving people out. Users that chose your product and trusted you. I'd love to see an open ticket, tracking the progress of this stabilization or asking for help to achieve that. At least. But they keep being closed (yes, I've been tracking this issue through several tickets already) |
This sounds perfect. I didn't find it referenced in the documentation. Am I missing it? |
It's currently marked as internal, so similar to an |
Such an interesting maintainers vs users debate. Let me share a few observations and let's be objective please. Navigation outside of React rendering tree is a basic application requirement. Applications aren't limited to React components. React is a rendering library and complete application needs to fill gaps which React leaves. Navigation is one of such gaps. As @brophdawg11 mentioned, browsers already implement history API and some implement navigation API. History package implements abstraction over history API. I liked the beauty of history package and React Router v5 where users have options to either use only React Router v5 or a combo of history package and React Router v5 in a way that you don't end up with a duplicate history package in the bundle (neat). @brophdawg11 solution shared by Ryan #9422 (comment) locks you into "Data router". Also, intercepting native browser back navigation locks you into "Data router" as well: #8139 (comment). As a result simply using basic routing features now results in the entire library ending up in the application bundle: #10354 (comment).
This is actually the other way around now, every non-trivial application ends up with 63kB RRv6 bundle vs 28kB RRv5: #10354. We're not even using data related features and it has nothing to do with the bundlers/configs, it's RRv6 code design decisions. Is it possible to clearly define boundaries what router should be doing vs what it shouldn't? Would it make sense to start another navigation library (like history) which will abstract browser history and navigation API and supersede history library? Would it make sense to split React Router library into two: one purely navigation-related and another data-related so that we don't end up dragging a lot of unnecessary code to be able to use basic navigation features? I'm not sure if majority of the users want a simple (from the usage perspective) package that used to provide an abstraction over navigation-related browser API in React projects to become an architectural jello: [Docs]: most ANNOYING doc I've ever seen in a long time for such a mainstream lib. |
Not being able to listen to Route changes natively is one of the reasons why Libraries like the history package were created in the first place. Hopefully, going forward the new Navigation API will solve that problem and make routing libraries less invasive https://developer.chrome.com/docs/web-platform/navigation-api/ |
In the future it will be possible to do the following: window.navigation.addEventListener("navigate", (event) => {
// do whatever you need to do in response to navigation
}); with the new Navigation API It's already in Chrome and Edge but will need some more time until it's in all Browsers |
Because of the different downsides of the mentioned approaches I took another "route". I am basically relying on browser event listeners to fire events when I want to navigate outside of router context. Then I have a listener inside the part of the app that has access to router. This listener then manages the navigation. It is a bit old fashioned but it worked out very well for me and I have had this code in production for months now without any complaints. If you want to see the code details have a look at the section "Navigating outside of Router's context" on my blog: https://frontendwithhasan.com/articles/react-router-upgrade#navigating-outside-of-routers-context. |
@brophdawg11 @ryanflorence this solution is only for those who are on this is what my app looks like
|
@brophdawg11 What is the recommended way to navigate outside of React components in a Remix app in SPA mode? I’m trying to redirect the user to the login page whenever the REST API returns 401 Unauthorized, via Axios interceptors. Since there is no Or, how can I get the |
@brophdawg11 A Define a loader like this import { replace, createBrowserRouter, type LoaderFunction } from 'react-router-dom';
const requireAuth: LoaderFunction = async ({ request }) => {
const currentUrl = new URL(request.url);
const params = new URLSearchParams({
from: `${currentUrl.pathname}${currentUrl.search}`,
});
// If the user is not logged in and tries to access a protected page, we redirect
// them to `/login` you can even use a `from` parameter that allows login to redirect back
// to this page upon successful authentication
if (isUserAuthenticated()) {
throw replace(`/login${params.toString()}`); // redirect to login page
}
return null;
}; Then wrap all your routes that require authentication like this const router = createBrowserRouter([
{ path: '/login', Component: LoginPage }, // do not require auth for login page
{ loader: requireAuth, children: [
// all routes that are supposed to require authentication
] } // pass loader
]) |
Hi @ChristophP. Thank you for your suggestion! I’m actually developing a single-page application so there is no The problem is that even though the frontend “thinks” the user is logged in, the session on the server side could have been timed out or deleted. So I needed a way to globally check the HTTP status code and redirect to the login page in case of 401, so that other parts of the frontend code doesn’t need to worry about the authentication status. That’s why I was trying to take advantage of Axios interceptors. Does your solution still work in my case? |
@luangong So as long as you can have an axios interceptor |
What could be the solution for this circular DEP problem? It breaks HMR in vite. I tried the solution with the injectRouter function, but it also breaks HMR in vite. |
What is the new or updated feature that you are suggesting?
The new
createBrowserRouter()
API internally callscreateBrowserHistory()
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 instantiatecreateBrowserHistory()
myself and provide the history object as an option tocreateBrowserRouter()
. Using the old,v6.3
api, doing something like this was possible via theunstable_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 owncreateBrowserHistory()
to it (using the newcreateBrowserHistory
export from"@remix-run/router"
, buthistory.listen()
only accepts a single listener#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).
The text was updated successfully, but these errors were encountered: