Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

[V6] [Feature]: Support absolute paths in descendant <Routes> #8035

Closed
Patrik-Lundqvist opened this issue Sep 14, 2021 · 31 comments
Closed

[V6] [Feature]: Support absolute paths in descendant <Routes> #8035

Patrik-Lundqvist opened this issue Sep 14, 2021 · 31 comments

Comments

@Patrik-Lundqvist
Copy link

Patrik-Lundqvist commented Sep 14, 2021

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

@timdorr
Copy link
Member

timdorr commented Sep 14, 2021

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.

@Patrik-Lundqvist
Copy link
Author

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 👍

@openscript
Copy link

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:

  1. Install dependencies and start project (yarn install and yarn start)
  2. Go to http://localhost:3000/customer/users
  3. Click on Next page (absolute)
  4. 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

@timdorr

This comment has been minimized.

@openscript

This comment has been minimized.

@julioflima

This comment has been minimized.

@mjackson mjackson changed the title [V6] [Feature]: Support absolute paths in nested <Routes> [V6] [Feature]: Support absolute paths in descendant <Routes> Sep 24, 2021
@thedanwoods
Copy link

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.

@osddeitf
Copy link

osddeitf commented Nov 6, 2021

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.

@openscript
Copy link

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.

@ryanflorence
Copy link
Member

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.

@jampy
Copy link

jampy commented Dec 20, 2021

Any news on this?

@jampy
Copy link

jampy commented Dec 20, 2021

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.

@jampy
Copy link

jampy commented Dec 20, 2021

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

@henrywoody
Copy link

henrywoody commented Dec 20, 2021

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

@jampy
Copy link

jampy commented Dec 20, 2021

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

@Paul-Yves
Copy link

Same issue here, #7992 only seems to work with nested routes, not descendant <Routes> component

@nunoleong
Copy link

nunoleong commented Feb 24, 2022

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

@lvilasboas
Copy link

lvilasboas commented Mar 3, 2022

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

@Kamahl19
Copy link

@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

@Kamahl19
Copy link

Kamahl19 commented May 16, 2022

@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

@jampy
Copy link

jampy commented May 20, 2022

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.

@otakustay
Copy link

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.

@hastom
Copy link

hastom commented Jul 14, 2022

@jampy thank you so much. You've saved me a hell lot of time <3

@bfaulk96
Copy link

bfaulk96 commented Aug 8, 2022

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.

@TeddyBurnsides
Copy link

TeddyBurnsides commented Sep 2, 2022

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.

@bel0v
Copy link

bel0v commented Sep 8, 2022

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.

@lvilasboas-dti
Copy link

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

@JakeSidSmith
Copy link

For my use cases I only require matching an absolute path from a flat list of Routes 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};

@hvolschenk
Copy link

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.

@justinbhopper
Copy link

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.

@brophdawg11
Copy link
Contributor

I'm going to convert this to a discussion so it can go through our new Open Development process. Please upvote the new Proposal if you'd like to see this considered!

@remix-run remix-run locked and limited conversation to collaborators Jan 9, 2023
@brophdawg11 brophdawg11 converted this issue into discussion #9841 Jan 9, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests