I want to be able to do the following
routes.js
<Router>
<Route path="/some-url" element={<SomeElement />}>
<Route path="/some-url/some-other-url" element={<SomeOtherElement />}>
</Route>
</Router>
SomeElement.js
<div>
<h1>SomeElement</h1>
<Outlet />
</div>
SomeOtherElement.js
<div>Some other element</div>
And if user lands on /some-url/some-other-url, he should see
<div>
<h1>SomeElement</h1>
<div>Some other element</div>
</div>
Motivation:
All paths in our app are defined as absolute paths like this
paths.js
export const SOME_URL = '/some-url'
export const SOME_OTHER_URL = '/some-url/some-other-url'
A nested route with an absolute path that does not exactly match its parent routes' paths is not valid, so any child routes that you define in this way will have to match their parent paths exactly. While we could technically enforce this, it doesn't seem like it provides much benefit.
Why not just use relative URLs?
Well, I have two reasons for not using relative URLs.
<Link to="something" /> you can't immediately tell where this link actually leads to.I second everything @smashercosmo says.
Most frontend-y apps (including ours) have been built with some routes.js file with absolute paths:
/
/users
/users/:id
etc.
Rewriting all of this would be painful.
We currently would like to do something like this:
// Top level App.tsx
<Router>
<Routes>
<Route path="/:teamSlug/*" element={<Team />} />
</Routes>
</Router>
// "Layout" for all things in teamSlug
const Team = () => (
<div>
<Sidebar />
<Routes>
{/* These absolute paths won't work when this is nested inside <Team /> */}
<Route path="/:teamSlug/settings" element={<TeamSettings />} />
<Route path="/:teamSlug/:projectSlug" element={<Project />} />
</Routes>
</div>
)
I'm kinda new to React Router: there might be alternate ways of achieveing this.
One of the main reasons we've got absolute routes is because of named routes with typed parameters. With Typescript, we can have a type of all routes, as well as their parameters:
// routes.ts
interface Team {
to: 'team',
params: { teamSlug: string },
}
interface Project {
to: 'project',
params: { teamSlug: string, projectSlug: string },
}
export type AppRoute = Team | Project;
// Used as: <Route path={routes.team} /> in app code
export const routes: { [name: string]: string } = {
team: '/:teamSlug',
project: '/:teamSlug/:projectSlug',
}
```tsx
import { routes, AppRoute } from './routes';
import {
Link as RouteLink,
LinkProps as RouteLinkProps,
} from 'react-router-dom';
// Link.tsx
type LinkProps = AppRoute & Omit
export const Link: React.FunctionComponent
let path = routes[to]; // path=/:teamSlug
if (params) {
for (const [key, val] of Object.entries(params)) {
path = path.replace(`:${key}`, (_) => String(val));
}
}
return
};
```tsx
// app code
import { Link } from './Link';
<Link to="team" params={{ teamSlug: "foo" }}>Team</Link>
<Link to="project" params={{ teamSlug: "foo", projectSlug: "bar" }}>Project</Link>
{/* Won't compile */}
<Link to="project" params={{ teamSlug: "foo" }}>Missing param</Link>
<Link to="foo" params={{ teamSlug: "foo", projectSlug: "bar" }}>Incorrect `to`</Link>
I hear you about rewriting all your existing route paths. I'd like to avoid that if we can.
One case that I think will be very confusing for people is with path="/" for "index" routes. It feels quite natural in a nested route to use <Route path="/"> to indicate a nested route that matches the URL up to that point, but no more. For example:
<Route path="settings" element={SettingsPage}>
<Route path="/" element={<SettingsIndex />} />
</Route>
In this case, it's pretty clear that path="/" means "match the /settings URL" (remembering we always ignore trailing slashes when matching paths), whereas "match the / (root) URL" wouldn't make any sense at all here.
Perhaps, since you have all absolute paths, you could dynamically generate your routes by looping over your array/object in a map?
Perhaps this could be solved by an absolute prop on the <Route /> component? Which by default is false, resulting in the behavior as you described. But if you know what you are doing and you have a more complex routing setup the path can be treated as an absolute path like it worked in previous versions. That way you don't have to deal with certain "meanings" in the path prop like a / or a . and it's easier to see what's going on.
One case that I think will be very confusing for people is with path="/" for "index" routes. It feels quite natural in a nested route to use
<Route path="/">
This doesn't look natural to me at all. Path, that starts with forward slash, always indicates that this is a root path. IMHO using path-less routes for index routes feels much more natural.
<Route path="settings" element={SettingsPage}>
<Route element={<SettingsIndex />} />
</Route>
Perhaps this could be solved by an absolute prop on the
component
Good idea, but I think it should be on <Routes /> or even on <...Router /> component. Something like
<Routes mode="absolute">
<Route path="/some-url" element={<SomeElement />}>
<Route path="/some-url/some-other-url" element={<SomeOtherElement />}>
</Route>
</Routes>
What does your current route config look like in v5 @smashercosmo? Are you doing the whole thing in JSX with absolute paths?
In case this is helpful for anyone, what I'm doing is just wrapping my absolute paths in a function which converts them into the relative form expected by <Route>
const route = (absolutePath: string) => absolutePath.substr(path.lastIndexOf('/') + 1)
Keeps the readability benefit and means if you have all your absolute paths defined in one place already, you don't have to refactor that and can just use it as-is.
From @smashercosmo example, it'd look like this
<Route path={route("/some-url")} element={<SomeElement />}>
<Route path={route("/some-url/some-other-url")} element={<SomeOtherElement />}>
</Route>
Or if you had paths defined in a paths constant or similar:
<Route path={route(paths.someUrl.base)} element={<SomeElement />}>
<Route path={route(paths.someUrl.someOtherUrl)} element={<SomeOtherElement />}>
</Route>
Super simple and, for me, does the job. I guess it might be nice to support absolute mode as a native feature but that adds API weight and complexity, and I guess this pattern might be an acceptable alternative that already works?
I think the big problem is, that in RRv6 / is used to mark the root of some sub-route. Therefore you cannot distinguish if it points to top-level root or to the root of some sub-route. Therefore a better solution would be either the empty string or . to point to the root of a sub-route. Then RRv6 could add absolute paths without a clash of meanings.
@MeiKatz just to clarify was that in response to my post? :-)
@davnicwil Kind of. I just wanted to point out, why this change could make some confusions if done wrong.
@MeiKatz I agree. At least how I'm thinking about it is that the trailing /, similar to *, is just a react-router specific configuration expression.
I therefore don't try to configure my 'root' components with actual paths from my paths constant, I just hard code them into the Routes like <Route path="/" ..., just as I hard code * chars like <Route path={paths.something.base + '/*'}... - I want to keep these 'special characters' out of my actual path definitions, only using them in the react-router Route definitions.
What's confusing is that, unlike *, / looks like a legit part of an actual path and appears inconsistent. I.e. why does the root path have a leading / but no other sub paths do?
I agree that a good way to solve this might be to make this 'root' special char something different, something more obviously a special char, like . as you suggest.
Weirdly though, to sidestep this completely, playing around I've noticed that in fact both leaving path out and setting path="" also seem to work for a root - see here - @mjackson I'm not sure if this is intended behaviour but if so these could also work as pretty decent alternatives?
Are absolute paths in <Route /> likely to become a feature? It seems useful and harmless to provide some sort of absolute prop.
We have rather a lot of routes, and we define them a bit like this:
export const ROUTES = {
HOME: '/',
SOME_MODULE: {
INDEX: '/some-module',
FEATURE: {
INDEX: '/some-module/feature',
ITEM: '/some-module/feature/:id',
getItemRoute: id => `/some-module/feature/${id}`,
},
},
};
The path and the function that builds the path are next to each other because they're two sides of the same coin: one is used for a <Route /> and the other for a <Link />. getItemRoute needs to return an absolute path, because it is meant to be used by any component, anywhere, that wants to point to /some-module/feature/:id.
It seems weird and brittle to then have to define a separate relative path for a <Route /> when the reality is you always mean an absolute path.
@mjackson We've now spent some time trying to making this work in our app, and having to use relative paths in <Route> is not a pleasant experience.
It's very helpful to be able to say, from anywhere, <Link to={SOME_GLOBAL_CONSTANT}>, and guarantee that it points to the same place as <Route path={SOME_GLOBAL_CONSTANT}>. This pattern makes a messy real world app far easier to maintain, as we have a lot of confidence that we aren't breaking navigation.
As it is, without absolute paths as an option, <Route path="/somewhere"> and <Link to="/somewhere"> mean completely different things.
The only solutions I can think of are:
(a) define all the path constants twice, one absolute and one relative (feels brittle); or
(b) whenever we have <Routes> grab the current location.pathname and strip that off the front of the path constant (feels like a hack).
Am I missing something? Being able to say <Route absolute path={SOME_GLOBAL_CONSTANT}> would solve all this in a jiffy.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
You can add the fresh label to prevent me from taking any action.
@timdorr do you mind adding fresh label to the issue?
There you go.
With the latest TypeScript features (https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types), using absolute urls across application becoming even more relevant. Now we can do something like this:
paths.ts
export const USER_POST_URL = '/users/:userId/posts/:postId'
UserPost.tsx
```tsx
import { useParams } from 'react-router-dom'
import * as PATHS from '../paths.ts'
import type { Params } from '../types.ts'
function UserPost() {
const { userId, postId } = useParams() as Params
}
Most helpful comment
@mjackson We've now spent some time trying to making this work in our app, and having to use relative paths in
<Route>is not a pleasant experience.It's very helpful to be able to say, from anywhere,
<Link to={SOME_GLOBAL_CONSTANT}>, and guarantee that it points to the same place as<Route path={SOME_GLOBAL_CONSTANT}>. This pattern makes a messy real world app far easier to maintain, as we have a lot of confidence that we aren't breaking navigation.As it is, without absolute paths as an option,
<Route path="/somewhere">and<Link to="/somewhere">mean completely different things.The only solutions I can think of are:
(a) define all the path constants twice, one absolute and one relative (feels brittle); or
(b) whenever we have
<Routes>grab the currentlocation.pathnameand strip that off the front of the path constant (feels like a hack).Am I missing something? Being able to say
<Route absolute path={SOME_GLOBAL_CONSTANT}>would solve all this in a jiffy.