react-router
react-router copied to clipboard
[V6] [Feature]: Support absolute paths in descendant `<Routes>`
What is the new or updated feature that you are suggesting?
Great to see that absolute paths will be supported in V6, I find it very handy to work with.
For it to be fully supported I believe it should work when using nested <Routes>
as well.
const App = () => (
<Routes>
<Route path="/users/*" element={<Users />} />
</Routes>
);
const Users = () => (
<Routes>
<Route path="/users/:id/settings" element={<UserSettings />} />
</Routes>
);
Currently (v6.0.0-beta.4) the route definition in the nested <Routes>
will match on a relative route, even though they start with /
indicating an absolute route.
Why should this feature be included?
This way we can use absolute paths throughout our application when we don't specify all routes in the same <Routes>
component. This will also make the migration from v5 to v6 much easier as this pattern is supported there using <Switch>
.
I'm not sure if this makes sense. If you want to use your Users routes in multiple places, you wouldn't be able to. Not to mention Links/NavLinks would be harder to reason about.
We treat the the route context that you render your Route within as a "basename" of sorts. That enables you to not have to worry about the context you're within when creating your Routes tree. It's easy enough to reason about absolute routes when they're all being rendered within the same component. But if you spread that out over different files or modules (or heck, even different repos), it becomes harder to keep track of it all.
Realistically, this is less about the Users component and more about the App component. How does the App component know the Users routes aren't going to escape the path App thinks they are nested under? That might be surprising at best and error-inducing at worst. I could understand some sort of escape hatch API, such as an absolute
prop, but I'd be hesitant to add it. It feels like a footgun.
I agree that isolating each routing context will make the components more reusable and should probably be the preferred way of doing things. But I also see the use of having routes specified in one place which then can be used by both <Link>
and <Route>
, especially in applications that have a lot of cross-cutting links and isn't on the "multiple repos" scale.
If this is not at all desired then I agree that an escape hatch might at least be a way ease the migration from v5 -> v6. Like stated here its somewhat common and quite a painful rewrite to do, but perhaps its necessary.
Thank you for the great work that's being done here 👍
I've already been posting about this in #7972, but I agree, that this was probably the wrong place to mention this. Hopefully now I'm on the spot.
The feature which @Patrik-Lundqvist is asking for, already existed in version v6.0.0-beta.1
but stopped working in v6.0.0-beta.2
. Here is a working reproduction with version v6.0.0-beta.1
: https://github.com/openscript/react-router-nested-routes-bug
Steps to reproduce:
- Install dependencies and start project (
yarn install
andyarn start
) - Go to
http://localhost:3000/customer/users
- Click on
Next page (absolute)
- The URL should be
http://localhost:3000/customer/users/10/2
With the branch not-working
and v6.0.0-beta.2
the URL will be: http://localhost:3000/10/2
BTW, you can open that in your browser with Codesandbox: https://codesandbox.io/s/github/openscript/react-router-nested-routes-bug
It's a neat feature from codesandbox, but it never worked for me:
:(
BTW, you can open that in your browser with Codesandbox: https://codesandbox.io/s/github/openscript/react-router-nested-routes-bug
Thanks, I was getting a cold sweat. I was having a problem not adding the slash after the path like: "users" to "users/".
I got the error:
Error: Absolute route path "/" nested under path "/users" is not valid. An absolute child route path must start with the combined path of all its parent routes.
useRoutes/matches<
/home/juliolima/packages/react-router/index.tsx:1012
But may you should update this, that way is not working at all. https://reacttraining.com/blog/react-router-v6-pre/
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="users" element={<Users />}>
<Route path="/" element={<UsersIndex />} />
<Route path=":id" element={<UserProfile />} />
<Route path="me" element={<OwnUserProfile />} />
</Route>
</Routes>
</BrowserRouter>
I agree that this is a necessary feature. Giving a <Routes>
a magic basename
that doesn't respect a leading /
is counterintuitive, and means we can't use path constants properly.
If we want to use absolute paths, we have to do everything in the same component:
<Routes>
<Route path="/clothes" element={<Clothes />}>
<Route path="/clothes/shirt" element={<Shirt />} />
<Route path="/clothes/trousers" element={<Trousers />} />
</Route>
</Routes>
...
const Clothes = () => (
<>
Clothes page
<Outlet />
</>
);
and we can't break it down into sub-routes components like this:
<Routes>
<Route path="/clothes/*" element={<Clothes />}></Route>
</Routes>
...
// This doesn't work, as it will try to match /clothes/clothes/shirt
const Clothes = () => (
<>
Clothes page
<Routes>
<Route path="/clothes/shirt" element={<Shirt />} />
<Route path="/clothes/trousers" element={<Trousers />} />
</Routes>
</>
);
The use cases are:
(1) We use constants for pathnames, to always guarantee a <Link to={PATH_CONSTANT}>
takes us to <Route path={PATH_CONSTANT}>
. Without this feature, we can't use PATH_CONSTANT
- we have to use something else.
(2) In a large application, if a developer is told there's a bug in /some/old/obsucre/page, a simple text search for "/some/old/obsucre/page"
immediately tells them what's being rendered.
(3) We organise our app in modules, and code-split by module, and don't want to put everything in an enormous top-level <AllTheRoutes />
component.
It's exciting to see React Router reach v6. But... As stated in https://remix.run/blog/react-router-v6:
Note: Absolute paths still work in v6 to help make upgrading easier. You can even ignore relative paths altogether and keep using absolute paths forever if you'd like. We won't mind.
I went all the way to upgrade my project to v6, but absolute path wouldn't work. It's pain in the *ss for me to use relative path, it's counter-intuitive.
I would love to get started to work on this. It would help me a lot if somebody can outline a broad idea how and where to implement this.
We justified supporting absolute paths in nested route configs, seems like the same reasoning applies here. It would also help migration from v5.
Unless @mjackson wants to talk me out of it, I'm all for it.
Any news on this?
What about extendeding the <Routes>
component with something like fromRoot
that forgets everything about parent <Routes>
?
Implementation in react-routes should be easy, I think, as it simply means to ignore parent matches.
Upgrading from v5 would be much easier (just add another prop):
const Clothes = () => (
<>
Clothes page
<Routes fromRoot> // <---- "fromRoot" makes this independent from parent <Routes> elements
<Route path="/clothes/shirt" element={<Shirt />} />
<Route path="/clothes/trousers" element={<Trousers />} />
</Routes>
</>
);
Children <Routes>
in <Shirt/>
could then still be relative unless fromRoot
is used there, too.
BTW, I would strongly suggest to add a __DEV__
warning whenever a <Route>
matches a path with leading /
but is relative to a parent route. It took me a while to figure out why my absolute routes did not work while upgrading from v4. IMHO relative matches to a absolute link notation are unintuitive.
Here is a similar, hacky solution.
<RootRoutes>
is a drop-in replacement for <Routes>
that ignores parent routes, with the effect of treating all <Route>
as absolute.
import { UNSAFE_RouteContext as RouteContext } from 'react-router';
function RootRoutes(props) {
const ctx = useContext(RouteContext);
const value = useMemo(
() => ({
...ctx,
matches: []
}),
[ ctx ]
);
return <RouteContext.Provider
value={value}
>
<Routes {...props}/>
</RouteContext.Provider>;
}
But please note this makes use of "undocumented" react-router
API and should not be used except for testing.
I still plead for a dedicated parameter for standard <Routes>
.
@jampy Support for absolute paths in nested <Route>
components was discussed in #7335 and added in #7992, seems the approach for absolute paths in descendant <Route>
components should follow the same pattern.
Edit: To make this more clear, by "should" I mean "when implemented" rather than "currently".
@henrywoody thanks for your reply! Good to know.
I've checked the PR and also the documentation, but I fail to understand how this is supposed to work. Do I need to specify something special to make nested absolute paths work?
My base component looks something like this:
function Application() {
return <Routes>
<Route path="/info/*" element={<InfoModule/>} />
/* ... */
<Route path="*" element={<Navigate to="/"/>} />
</Routes>;
}
and the nested routes look basically like this:
function InfoModule() {
return <Routes>
<Route path="/info/" element={<WelcomePage/>} />
<Route path="/info/docs/*" element={<DocsBrowser/>} />
</Routes>;
}
However, neither <WelcomePage>
nor <DocsBrowser>
render with this config (but <InfoModule>
does).
Logging useLocation()
in InfoModule
shows {pathname: '/info', search: '', hash: '', state: null, key: '3cuoedw8'}
or {pathname: '/info/docs', search: '', hash: '', state: null, key: 'default'}
when I manually change the URL hash (I'm using HashRouter). They look good to me.
Swapping <Routes>
with my <RootRoutes>
hack above makes the routes render as intended, OTOH.
What am I doing wrong?
Same issue here, #7992 only seems to work with nested routes, not descendant <Routes>
component
I went with @jampy 's hack and migration was mostly flawless with almost no code changes required. I've done extensive testing (my app has around 110 routes spread on nested RootRoutes
) and everything is working.
/edit
just to add the reason is similar to what has been mentioned above: centralized string constants to handle everything related to routing:
-
<Route path>
-
<Navigate to>
-
navigate()
-
<Link to>
- ...
Any news on this? Besides @jampy 's hack?
I'm using v6.2.2
And the reason I'm looking for this feature is precisely what @nunoleong has mentioned (centralized string constants):
just to add the reason is similar to what has been mentioned above: centralized string constants to handle everything related to routing:
<Route path>
<Navigate to>
navigate()
<Link to>
- ...
@ryanflorence @mjackson Any news on this feature? Is it maybe in progress or should community provide a PR? I believe this is the main blocker for existing apps to upgrade
@jampy would you please provide a simple example or a working demo of your hack? I am unable to make it work with absolute nested paths. Thanks a lot! cc @nunoleong
Edit: ok I made it work using the hack. Before I only replaced the top Routes with RootRoutes but it's necessary to replace all of them. This however breaks index routes when using the index
flag
Edit: ok I made it work using the hack. Before I only replaced the top Routes with RootRoutes but it's necessary to replace all of them. This however breaks index routes when using the
index
flag
You don't need to replace the top Routes. Just use <RootRoutes>
when you whant to "disconnect" those routes from any parent <Routes>
or <RootRoutes>
elements.
I have a use case that, I have 2 "conflicting" routes like:
-
/users/:tab
to render descriptive information using tabs. -
/users/:id
to render detail information for a specific user.
Since react-router@6
removes RegExp routes, I have to combine them into a single route /users/:placeholder
:
function Entry() {
const {placeholder} = useParams();
if (placeholder === 'tabA' || placeholder === 'tabB') {
return (
<Routes>
<Route path="/users/:tab" element={<UsersPage />} />
</Routes>
);
}
return (
<Routes>
<Route path="/users/:id" element={<UserDetail />} />
</Routes>
);
}
<Routes>
<Route path="/users/:placeholder" element={<Entry />} />
</Routes>
Nested <Route />
in this example is to "rename" params in route paths in order to make UsersPage
and UserDetail
able to access :tab
and :id
correctly.
@jampy thank you so much. You've saved me a hell lot of time <3
Any update on this? I have a specific use-case involving module-federated apps where absolute routes are necessary unless I am to refactor an entire project:
In App A:
- `/my-route/test` -> render App B
- `/my-route/another` -> render App B
- `/my-route/route` -> render App B
- `/my-route/different` -> different component
- `/my-route/*` -> default component or redirect
then in App B:
- `/my-route/test` -> test component
- `/my-route/another` -> another component
- `/my-route/route` -> route component
App A needs to know what sub-routes to send to App B, but because of the way RR6 works, App B can no longer use the sub-routes for routing as they've already been resolved in the path. Without allowing descendent routes to use absolute routing, I don't think there's a way to implement this pattern using RR6 unless I remove real routing from App B entirely and just force it to render different components with some route="test"
prop, which feels pretty gross to me.
Update:
Figured out a way to get around this using useMatch({ path: '/my-route/:route' end: false });
in App B, and then using a route map object. It's less than ideal but works as a workaround for the time being I suppose.
I'm a little baffled that this is still an issue with V6. How is any medium to large sized project that uses absolute paths supposed to upgrade? It's unfeasible to completely rebuild my application to support inheriting parent paths.
We're also waiting for this feature. Although we don't have a problem of migrating an old large project, we still think that hardcoded pieces of routes are hard to work with in the long run — we often need to link from one nested route to another, and the only way to do this now seems to hardcode all the hrefs. e.g. imagine a special page
dashboard/item/:id/edit/address
that is composed of dashboard
, item
, edit
and address
all scattered among small <Routes>
. Now say I want to have an edit item's address
link anywhere in the app. The only way of doing it seems to be hardcoding a string, which is very fragile.
Almost a year has gone by since this issue has been opened, a ton of users have presented strong arguments on this as well, and absolutely no answer so far...
For my use cases I only require matching an absolute path
from a flat list of Route
s and rendering an element
, so we're using the following. It ain't too smart but maybe peeps can modify it to support more features.
import {ReactElement} from 'react';
import {matchPath, useLocation} from 'react-router-dom';
// Allows matching `Route`s against absolute paths when they are nested under another `Route`
// See: https://github.com/remix-run/react-router/issues/8035
const AbsoluteRoutes = ({children}: {children?: readonly ReactElement[]}) => {
const {pathname} = useLocation();
return (
children?.find(
child =>
typeof child.props.path === 'string' &&
matchPath(child.props.path, pathname),
)?.props.element ?? null
);
};
export {AbsoluteRoutes};
For the meantime I ended up with these two methods to keep my absolute paths (sort of):
export const urlLayout = (url: string): string => `${url}/*`;
export const urlRelative = (url: string, parent: string): string => url.replace(parent, '');
I have created a sample sandbox of how they are used here: codesandbox.
V6 has conflicting philosophies in its new Path architecture.
In one hand, absolute paths are heavily embraced by utilizing the power of TypeScript's Template Literal types, even to the point of removing heavily used features.
On the other hand, absolute paths (and template literal types) are discouraged because nested routes simply don't work with them at all.