-
-
Notifications
You must be signed in to change notification settings - Fork 10.2k
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
Enable ability to have nested MemoryRouters #9112
Enable ability to have nested MemoryRouters #9112
Conversation
|
Hi @auaustorg-ms, Welcome, and thank you for contributing to React Router! Before we consider your pull request, we ask that you sign our Contributor License Agreement (CLA). We require this only once. You may review the CLA and sign it by adding your name to contributors.yml. Once the CLA is signed, the If you have already signed the CLA and received this response in error, or if you have any questions, please contact us at hello@remix.run. Thanks! - The Remix team |
Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳 |
Tagging a few folks who look like can review PRs. This PR has been up for about 2 weeks and I was hoping to get some feedback. Thank you! |
I'll be honest: I don't think this is going to be merged in. At a high level, you're creating scopes for Context values, which is not really how Context is meant to be used. Instead, you should simply re-use the same Provider with a new value and now you have a new scope. Instead, you should just be able to nest a Sorry to be a Negative Nancy, but I'd rather be realistic here. I do appreciate the hard work that went into this PR. I think we can take some of the intent here and turn it into something more likely to be merged. |
Hey @timdorr, thank you for the response. I wanted to clarify why I had to add There are use cases where the flow might be like the following: BrowserRouter -> HomePage -> [MemoryRouter -> Modal1 -> Modal2] -> SettingsPage Where you start on the HomePage, get shown Modal1 and Modal2 (both modals inside a memory router) and on Modal2 there is a button to take you to a SettingsPage which is now back in the BrowserRouter context. If Modal2 is like: function Modal2() {
const navigate = useNavigate();
return <button onClick={() => navigate(...)}>Go to Settings Page</button>
} Clicking the button would do nothing because const {
MemoryRouter: ScopedMemoryRouter,
useNavigate: useScopedNavigate,
} = createScopedMemoryRouterEnvironment();
// ... use ScopedMemoryRouter inside of a <Route>, etc
function Modal2() {
const navigate = useNavigate();
const scopedNavigate = useScopedNavigate();
return <>
<button onClick={() => navigate(...)}>Go to Settings</button>
<button onClick={() => scopedNavigate(...)}>Go to Modal 3</button>
<>
} This is why we need |
@timdorr if you want to chat more about this on the Remix discord or some other medium let me know. Would love to work and contribute towards a solution. |
Hey @timdorr, just wanted to ping on this one since it's been a few weeks. We'd love to help improve react-router all-up and think this could solve the common issue of wanting a nested memory router inside of the broader browser router. Let us know if we can help provide more clarification on why a pattern like |
Again, I think this is overcomplicating the issue. The problems with nested hooks like I guess I'm not understanding why there is a need for another new instance of a |
Thanks for the response @timdorr! Apologies if we haven't done the best job of explaining what problem space this is addressing. Imagine you have a "normal" web application that has routes driven by the browser's address bar. The routes in this app could be things like a home page, user profile page, other content pages, etc. Existing react-router patterns apply here (including Now imagine you'd also like to have a Settings panel inside of this application that can be invoked from any route on the website and sits on top of the app like a modal. Interactions and navigations within this panel are isolated to the panel and don't impact the broader application / browser's URL. This Settings panel has multiple screens within it (such as Account, Video, Audio, Accessibility, etc.) that can link to one another and be directly linked to from various parts of the larger application. Additionally, these screens may also have "L2s" or sub-screens that dive deeper into the panel... If a top-level route in the Settings panel might be defined as This is all to illustrate that if you're ever trying to build a route-driven application that is embedded within a broader "typical" application it would be a perfect use-case to have a nested MemoryRouter so that you could have all of the benefits of the react-router library inside of the embedded app. This PR also leaves the door open for the embedded applications to still navigate the top-level app using the default The highest level use-case I can sum this up as is whenever you have overlays and they're serious enough to benefit from their own router / history stack this would be a really welcome feature to utilize. We've run into three use-cases in our own website that would benefit from this pattern (the Settings panel mentioned above, a fly-out "Social" panel that a user can invoke anywhere on the site and it has numerous routes and sub-routes inside of it, and a general pattern for modals and being able to easily route to a specific one or supporting sub-states). Hopefully this helps frame the problem we're attempting to solve here. Are there other alternatives you see to our problem that we might have missed? Any further questions or context I can help provide? |
@timdorr Friendly ping. Any thoughts on @ElliotChong-MS comment? Thank you. |
I'm interested in this conversation but am a bit busy with the 6.4 release. I've only skimmed a few comments. Just FYI: you can create a brand new React Tree inside of an effect to accomplish what you're after (don't use a portal, that'll share context, the point is to get rid of it). let ref = useRef();
useEffect(() => {
ReactDOM.render(<MemoryRouter />, ref.current);
}, []);
return <div ref={ref}/> Or something like that ... My big question about these scoped router trees is how do you link back out to the browser router inside the context of a scoped router? Like Tim, it's also unclear to me why you can't just add some more normal routes? Again, I haven't had time to read all of the comments, but just wanted to toss out that quick solution to your problem that (should?) work today and a couple questions to think about. |
Hey @ryanflorence, thanks for the comment! Interesting solution with embedding a new React tree, but I think that would have the issue of being unable to navigate the root BrowserRouter from within the nested React tree's MemoryRouter. Re: your question on the different interactions: The const {
MemoryRouter: ScopedMemoryRouter,
Routes: ScopedRoutes,
Route: ScopedRoute,
useNavigate: useScopedNavigate,
Link: ScopedLink,
} = createScopedMemoryRouterEnvironment(); If you'd like to navigate the root BrowserRouter you'd use the regularly imported hooks from the lib and if you're navigating within the scoped memory router I'd anticipate you'd run this in a module in user-land code and re-export out these generated values, i.e.:
export const {
MemoryRouter,
Routes,
Route,
useNavigate,
Link,
} = createScopedMemoryRouterEnvironment();
import { useNavigate } from '../scoped-react-router';
... The reason we don't want to add normal routes is that the UX we're controlling is global and not tied to the route (it could appear on top of any page / route) and also it shouldn't really impact the browser's URI, it's truly in-memory routing. I've tried to frame the context there in the last comment with a few real-world use-cases we're running into but if they're not coming across that clearly let me know and I'll take another swipe at it. Thanks a lot for your suggestions and questions, let me know if there is any other context we can provide! |
One option is to pass the browser navigate as it's own context, something like https://stackblitz.com/edit/github-8cynrp-ekm7kv click on Modal link, or change route to /modal But yeah, great idea @ryanflorence with new React Tree 👏 |
We're running into this issue with a similar use-case where we're using a single "global" I've tried out the approach mentioned in #9112 (comment) of rendering a new React Tree but we're also using other contexts such as react-redux's Just commenting to make it apparent that there are more parties struggling with this and following any progress on this ✌🏼 |
On other hand why we need new tree? React context can stack, so basic passing MemoryRouter should override already provided contexts. Now we can reset BrowserRouter using UNSAFE_LocationContext, UNSAFE_RouteContext or am i missing something 🤔 https://stackblitz.com/edit/github-8cynrp-aiwgho?file=src%2FApp.tsx |
I'm looking into the option to stack MemoryRouters as well. It seems to me from reading through the thread that a lot of the pushback is around the need to link back to parent routers. In these very complex cases, I feel @piecyk suggestion of creating your own contexts makes sense here, rather than trying to bake things into react-router itself. @ElliotChong-MS @auaustorg-ms Have you thought of changing to use |
@ansonlouis How do you handle hooks correctly? Ideally you can use a |
@auaustorg-ms sorry, I actually realized after more testing that react-router simply throws an error if you try to nest However, to answer your question from a theoretical POV, you wouldn't be able to use the core react-router hooks in every situation. Since there can naturally only be one It seems like a lot of work, but it's really not. And, for a non-typical use-case such as nested routers, I think this seems reasonable. |
@ansonlouis I agree. This PR allows you to use hooks like |
@auaustorg-ms I see. Yeah it's not a bad idea and could be a good solution. It does bring in some good semantics for this specific use-case, which might prove helpful. I do agree with some others here that it adds complexity to the API for a non-typical use-case, though. I think fixing any of the bugs that exist from nested routers, as well as remove the explicit restriction of nested Memory/History routers would be a good start. That would at least allow you and me to implement nested routers in our own ways without adding complexity to the core API. |
@ryanflorence @timdorr I just caught this PR up to the latest changes. The functionality added in this PR is important to other users of react-router as well. I was hoping to get a better understanding of where you stand on the proposed changes in this PR? Ideally, we would love to merge these changes into react-router. At this time, we plan to maintain a fork with this additional functionality with the hopes to one day work with you to get this (or a version of this) functionality back into the project itself as we believe it will benefit a lot of users. I look forward to hearing back. Thank you! |
@auaustorg-ms mind linking to the npm module or repo for the fork in the meantime? It looks like the maintainers of this repo have gone silent but I'll +1 to exploring a solution for this that can be merged into react-router. |
Just wanted to ping on this as well. We're using a MemoryRouter with an embedded application, but want to expose different embedded versions at different browser routes. We're surprised this feature isn't supported in v6! |
Fixes remix-run#9548 This is a partial fix. Overflow should scroll within code blocks.
Would it be easier/possible to nest a MemoryRouter (Modal) inside a BrowserRouter today if you didn't need to link back to the BrowserRouter? |
No, this is not possible today due to how Context is currently set up within react-router. |
I saw this post: https://remix.run/blog/open-development I am hoping we can get some traction on this change. We also decided to not maintain a fork at this current time. Anyone who needs to have this functionality can use the changes here and publish their own fork. Hoping we can see a solution like this merged into react-router one day. Thanks all! |
👋 We're doing a little house cleaning to start the new year. Since we have a proposal for this now, and I think if that gets accepted it'll be in favor of a separate Thank you for all the hard work and exploration you put into this! |
@brophdawg11 can you link that proposal? Sorry just catching up here but we have a similar use case where one part of the application makes more sense in a Update: Ugh, I see now I just missed above 🤦♂️ |
Yep! #9601 |
Hello everyone!
My name is Austin and I am a dev on Xbox. I primarily work on Xbox Cloud Gaming. We currently leverage the
react-router
package and enjoy it.The Problem
We are looking to upgrade to v6 soon and at the same time we are doing some work around Modals and wanted to leverage a
MemoryRouter
to control a Modal stack. We thought it would be great to navigate our Modals similar to how we navigate the rest of the site.Key features include
Our desired features led us back to
react-router
. We did not want to re-invent the wheel and thought something like aMemoryRouter
would be perfect because we wanted our Modal routing system to not be exposed to the Browser. You cannot deep link into a modal or anything. It is just meant to be used for in-memory navigation, exactly the purpose of<MemoryRouter>
.So with that in mind, we went looking around to answer the question "Can you have a MemoryRouter inside of a BrowserRouter?". We ended up finding issues like the following:
#9109
#8817
#7375
The Solution
We realized it is not possible to nest Routers in v6 and it was a bummer. After looking through the
react-router
code, we came to the conclusion that is is definitely possible to enable having nested MemoryRouter's inside of other Routers. So here we are, with a PR that enables nesting MemoryRouter's.Example Usage:
You can see this example under the new
examples/scoped-memory-router
demo.How it works
There is a new method exported called
createScopedMemoryRouterEnvironment
which returns the various components / hooks needed for a scoped memory router.This function does not return things like
BrowserRouter
since it does not make sense to have nested BrowserRouter's.It works by making almost all components / hooks into create methods. Instead of having
useLocation
use the default context, we replaced it with acreateLocationHook
that is passed a context. We then use the default context for the defaultuseLocation
export, but it then allows us to create a newuseLocation
hook inside ofcreateScopedMemoryRouterEnvironment
.This creator pattern was taken from
react-redux
as seen in their useSelector implementation.We've leveraged
react-redux
createSelectorHook
before and had a great time working with that pattern. We believe this pattern can also be used inreact-router
and have proved it out in this PR. These proposed changes would resolve issues like:#9109
#8817
#7375
Notes For Reviewers
This is my first time contributing to a large open source project, so please bear🐻 with me. I still need to add more test cases, but wanted to get this review up to gather feedback early. I am open to any and all changes, including naming. As we know naming is one of the hardest things in CS 😜. I am not super familiar with all the various routers like the new DataRouter's, etc. I am not sure exactly what routers are allowed to be nested. Should
DataMemoryRouter
be nested? These are questions I am hoping you can answer. If you are open to these contributions, then I can write docs for this new feature, but I did not want to get ahead of myself.FAQ
Why do you need to create unique contexts?
The implementation in this PR allows navigation of different routers from any level of the application by allowing MemoryRouters to have distinct contexts.
The createScopedMemoryRouterEnvironment call returns scoped hooks that allow you to perform actions within the scoped MemoryRouter. You can see this at play in the example here:
If you'd like to navigate the root BrowserRouter you'd use the regularly imported hooks from the lib and if you're navigating within the scoped memory router I'd anticipate you'd run this in a module in user-land code and re-export out these generated values, i.e.:
src/embedded-application/scoped-react-router.ts
src/embedded-application/component/Foo.tsx
This allows us to navigate within the scoped Memory Router, but also navigate at the Browser Router level if needed.
Cheers!