remix-run / react-router

Declarative routing for React
https://reactrouter.com
MIT License
52.89k stars 10.24k forks source link

V6: Optional parameters #7285

Closed clarkd closed 4 years ago

clarkd commented 4 years ago

I tried adding a trailing ? to a route using the v6 router, but it didn't seem to work.

<Route path='/page/:friendlyName/:sort?' element={<Page/>} />

Are optional parameters supported in v6, or are they coming later?

timdorr commented 4 years ago

We don't plan on supporting them in v6.

clarkd commented 4 years ago

@timdorr Thanks - could you explain a little on how you would achieve it in v6? At the moment I have the below which works but seems less than optimal. Any thoughts?

<Route path='/page/:friendlyName/:sort' element={<Page/>} />
<Route path='/page/:friendlyName/' element={<Page/>} />
gopeter commented 4 years ago

It would be great if this could be described in the migration guide – or if there was at least a hint. So the recommended way of doing "optional" params really is ...

<Route path='/page/:friendlyName/:sort' element={<Page/>} />
<Route path='/page/:friendlyName/' element={<Page/>} />

... ?

MeiKatz commented 4 years ago

@gopeter Yes, or you can do something like this:

<Route path="/page/:friendlyName">
  <Route path=":sort" element={<Page />} />
  <Route path="" element={<Page />} />
</Route>
gopeter commented 4 years ago

Aaaaah, great, thank you very much! 😄

robbinjanssen commented 4 years ago

@MeiKatz @timdorr my problem with the approach above is that the <Page> component unmounts and mounts again. Performance-wise this might be a bad thing, but in my case it also results in a bunch of refetching of data. I can fix this inside the Page-component itself by checking if data is there already, but not only does this change break my routing, it also breaks application itself.

This is my use case;

<Route path="tickets">
  <Routes>
    <Route path="archive">
      <Route path=":ticketId/:content" element={<List />} />
      <Route path=":ticketId" element={<List />} />
      <Route path="/" element={<List />} />
    </Route>
    <Route path="view/:ticketId">
      <Route path=":content" element={<List />} />
      <Route path="/" element={<List />} />
    </Route>
    <Route path=":ticketId">
      <Route path=":content" element={<List />} />
      <Route path="/" element={<List />} />
    </Route>
    <Route path="/" element={<List />} />
  </Routes>
</Route>

A user starts at /tickets, then clicks on a link on the list that navigates to /tickets/1. This now re-renders the complete page where before it would re-render a part inside the component. Navigating from /tickets/1 to /tickets/2 works as expected.

Any other suggestions on how to implement this (Route-wise) without having to refactor the complete <List/>-component?

MeiKatz commented 4 years ago

@robbinjanssen Have you thought about using the <Outlet /> element?

<Route path="tickets" element={ <TicketsPage /> }>
  <Route path=":ticketId" element={ <ShowPage /> } />
  <Route path="" element={ <IndexPage /> } />
</Route>
const TicketContext = React.createContext( {} );

function TicketsPage() {
  const [ tickets, setTickets ] = useState({});

  useEffect(() => {
    // do some loading stuff
    const res = await fetch("/tickets");
    const tickets = await res.json();

    setTickets( tickets );
  }, []);

  return (
    <Fragment>
      <h1>My Tickets</h1>
      <TicketContext.Provider value={ tickets }>
        <Outlet />
      </TicketContext.Provider>
    </Fragment>
  );
}

function ShowPage() {
  const tickets = useContext( TicketContext );
  const { ticketId } = useParams();

  const ticket = tickets[ ticketId ];

  return (
    <div>{ JSON.stringify( ticket ) }</div>
  );
}
robbinjanssen commented 4 years ago

@MeiKatz not yet, hoped I would not have to refactor that ticket page, but seems like there's no other way. It's a pretty "old" component that requires a lot of work when refactoring. So I wish I could somehow keep the optional parameters :)

MeiKatz commented 4 years ago

@robbinjanssen Understandable. But I don't think that there is any other way you can solve it. And I also think, that it is a clean way to do it. Also we could think about dropping the whole context stuff and pass the parameters to the <Outlet /> and from there to the element.

robbinjanssen commented 4 years ago

For now I'll just parse the "raw" params into the bits I need, those URLs are not that complicated fortunately, and i'll put the component on the refactor list. Thanks :)

manterfield commented 3 years ago

Is there anywhere public that we can see the reasoning behind the choice to drop this feature? I appreciate the workarounds and help you've provided above @MeiKatz, but they're still a less ergonomic developer experience (IMO at least).

I can't imagine that you'd take that choice without good reason, but it'd be good to know what that was and if it's something that can be overcome?

happyfloat commented 2 years ago

Facing a similar issue. I want to open a Modal depending on the optional parameter. The Modal needs to be mounted already in order to show/hide correctly. I don't think using an outlet with immediate mount/unmount will work here. So I'm doing this as a work around. This way I can parse the route params in the HomePage Component, where the Modal is already mounted:

<Route path="/" element={<HomePage />}>
    <Route path=":activity_id" element={null} />
</Route>
n8sabes commented 2 years ago

Dropping v5-style optional parameters is requiring significant code changes to migrate from v5 to v6. Aside from the new v6 optional parameter patterns "seeming" a bit inefficient, it's also requiring architectural changes to the target application to avoid unnecessary component reloads (similar to what @robbinjanssen stated). As a result, it seems there is no clean migration path from v5 to v6 and may roll back to v5 until more time can be invested to pull projects forward to v6.

I want to emphasize, the work on react router is valuable and much appreciated. I too wish there was a bit more collaborative discussion on this before such an impactful change.

bthorben commented 2 years ago

I would like to chime in and second that this is a huge change. It means changing the architecture of an application. For our application, this will result in multiple weeks of work.

Kepro commented 2 years ago

same here, our application will need couple of weeks of refactoring because of missing optional parameters :( I was very excited about v6 but looks like v5 is still better choice for our team now :(

elliotdickison commented 2 years ago

Really appreciate the work on react router. This comment is meant to be feedback, not a complaint, but I'm starting a new project on v5 because of this issue. It's possible I'm just not the target audience of v6 (I'm using react-router in combination with react-navigation for a cross-platform app, so I've got compatibility issues to think about). v6 looks super awesome and I love the approach, I'm just not sure I understand why optional parameters don't fit in the paradigm.

vylink commented 2 years ago

Hi there, We are using dynamic routing(based on data from the server). The following path was simply perfect for our case.

<Route
  path="/:id?/:view?/:id2?/:view2?/:id3?/:view3?/:id4?/:view4?"
  render={() => <ReturnedComponent someParams={someParams}  />}
/>

Returned component decided what to do based on the optional parameters. Is there any way, how to achieve this kind of behaviour with V6?

queengooborg commented 2 years ago

I'm a bit surprised to see that support for optional parameters was dropped in v6. This is an extremely useful feature, and its removal has made a v5 to v6 project more challenging. The workarounds people here have suggested are great for smaller apps, but it's not clear why there has to be a workaround in the first place?

I'd love to hear the reasoning behind this change.

sridharravanan commented 2 years ago

@gopeter Yes, or you can do something like this:

<Route path="/page/:friendlyName">
  <Route path=":sort" element={<Page />} />
  <Route path="" element={<Page />} />
</Route>

its working

Kepro commented 2 years ago

@timdorr any reply that you want to add?

jrmyio commented 2 years ago

I will give my take on this because I find it rather unfortunate that in 2021 the most popular Javascript router doesn't support a pattern I've been using successfully for almost 2 decades.

As far as I can see from reactions and discussions in this and other issues, the reason optional parameters are not supported in v6 is because they don't work well with the idea of the / (in the URL) representing a step deeper into the application. In the context of Remix, the pathname of the URL is mainly to follow the hierarchy of your page, for example: User -> Messages > Inbox with the route user/messages/inbox.

When you want to use optional parameters, these are (almost) never related to the hierarchy of the page. Thus, it is suggested that you use query params. For example for ordering, filtering, pagination of the inbox you would start using the querystring: user/messages/inbox?page=1&order=email.

However, folks at @remix reference the old PHP days quite a lot, and they might remember that before the nginxes and node servers, people used apache's .htaccess rewriterule to rewrite urls like /$order/$page => to something like index.php?order=$order&page=$page, in order to get those clean urls. There were, and still are, reasons why people do not want to use the querystring for these kind of things, here are some advantages you get when not using the query string:

Now, I haven't put much thought in how to get the best of both worlds, and I realize that by keeping a nested <Route/> about the hierarchy of the page helps a dev understand what a route really means, and gives frameworks like remix an easier time doing its magic with these pages.

That's why I think adding an additional component to deal with a part of the pathname that is not related to the hierarchy (optional parameters), might be worth exploring.

For example, Something like <QueryPath/> ? This would differentiate it from being a regular <Route/>, and tell react-router it is simply a configuration to map the query params to a nicer pathname. This part of the pathname is expected to come AFTER the base pathname of the current parent Route. It could potentially also configure default values for each of its optional parameters, for example:

<QueryPath path={':city/:from/:till/:page'} defaults={{ city: 'all', from: 'any', till: 'any', page: 1 }} />

a seperate hook like useQueryPath, or maybe just the useParams could return the values for these params. Values that are their default could be left out, or set to null.

Since constructing these URLs is pretty hard it would be great if one can reuse the configuration (the path={':city/:from/:till/:page'} defaults={{ city: 'all', from: 'any', till: 'any', page: 1 }}- part) with a function like generateQueryPath which could generate this part of the pathname, removing all the default values used at the end.

I hope this is somewhat convincing that there are use-cases and I hope it helps moving this discussion further 🙄 .

n8sabes commented 2 years ago

Quick Optional Parameter Solution — Following up on prior post.

@jrmyio makes some good observations and worth the read. For the record, I love react-router v5. However, rearchitecting for v6 with what seem to be unintuitive code patterns (hacks) that necessitated deep app code/flow changes, simply to get existing value, required too much effort without meaningful new value.

After much head banging, hit a hard-stop with v6 and decided to try wouter. Ended up migrating to wouter quickly with very little effort as the wouter GitHub page is straight-forward, making it an easy migration guide to swap out react-router and regain optional parameters.

Hopefully there will be more time to spend with react-router v6, but for now wouter is meeting expectations, and is a smaller library. Keeping fingers crossed for optional parameter backward compatibility from v6!!

Maybe this issue should be re-opened?

ymoon715 commented 2 years ago

This doesn't feel like a react-router 6. This feels more like a new router with a new philosophy. I'm gonna have to stick to v5 and cross my fingers I find a worthy alternative. This isn't just a "breaking change". This is actually "must find alternatives" for a lot of people including me

PeteJobi commented 2 years ago

For those with the problem of pages unmounting and remounting on route change, I found out now that if you put your element on the parent <Route>, there will be no unmounts. That is, in the case of @robbinjanssen, if he changes the parent route

<Route path="tickets">
   ...
</Route>

to this:

<Route path="tickets" element={<List />}>
   ...
</Route>

It should work as he expects....

I can't say for sure though as this may depend on the implementation for <List/>, but it works for me.

saidMounaim commented 2 years ago

@gopeter Yes, or you can do something like this:

<Route path="/page/:friendlyName">
  <Route path=":sort" element={<Page />} />
  <Route path="" element={<Page />} />
</Route>

Thank youu

mmaloon commented 2 years ago

How about using the new "*" path catchall pattern? It looks like this works similar to an "optional" segment, and will be stored in the "*" key of the useParams hook return object.

<Route path='/page/:friendlyName/*' element={<Page/>} />
...
function Page() {
  const { friendlyName, '*': sort } = useParams();
  // `sort` will contain anything from the pathname after the friendlyName segment, or undefined
   ...
}
iamunadike commented 2 years ago

@gopeter Yes, or you can do something like this:

<Route path="/page/:friendlyName">
  <Route path=":sort" element={<Page />} />
  <Route path="" element={<Page />} />
</Route>

I would like to contrubute here by saying , the above is good but a bit ambiguous, imagine having to have multiple lines for single component,

my hints to help for boosting efficiency in v6:

These are just some concerned opinions and this should apply to routes.js with a seperated routes component in a seperate file

iamunadike commented 2 years ago

Furthermore imagine you have a file called routes.js in the react root folder and this has nested routes with nested routes ,and then you want to point the nested routes to optional params using this approach

<Route path="/page/:friendlyName">
  <Route path=":sort" element={<Page />} />
  <Route path="" element={<Page />} />
</Route>
routes = [
....omitted for brevity
    { path: "", element: <Main />,
  children: [
        { path: "/page/:type", element: <Page />,
        children: [
          { path:  ":status/:step", element: <PageForm /> },
        ],
      },
],

then writing the multiple routes would be a problem I guess because you may to repeat the whole nesting for the children of an already nested routes which would be cumbersome I suppose and boring so again the optional ? parameter wins except it can be set to arrays say :

routes = [
    { path: "", element: <Main />,
  children: [
        { path: ["/page/:type", "/page"], element: <Page />,
        children: [
          { path:  ":status/:step", element: <PageForm /> },
        ],
      },
],

N.B the above approach does not work at the time of this writing , React Router v6 being used

manterfield commented 2 years ago

@timdorr Could you spend a moment to give us some reasoning for this feature being removed? I understand it's a conscious choice, but is that due to development resources, conflicting with some other design choice, or something else?

With that information the community will be better able to move forward, either by someone stepping up and solving whatever problem you've seen with it, contributing the code, or just forking. At the moment we're all sort of in the dark which makes any community contribution difficult.

Given the number of issues opened about this and related features and the responses on each of those and here, it's clear it's a pretty important feature to users.

jamalkaksouri commented 2 years ago

In the v5 we could specify path like this:

<Route path="/products/:id1?/:id:?">
    <Products/>
</Route>

in v6, we have to register each route separately:

<Route path="/products/" element={<Products/>} />
<Route path="/products/:id1" element={<Products/>} />
<Route path="/products/:id1"/:id2" element={<Products/>} />
mikel-codes commented 2 years ago

I have an annoying issue with react routers: using this an example

routes = [
....omitted for brevity
    { path: "", element: <Main />,
  children: [
        { path: "orders/:type", element: <Orders />,
        children: [
          { path:  "new", element: <OrderForm /> },
        ],
      },
   { path: "orders/:type", element: <Navigate to="orders/:type/?page=1&items=8" /> }

],

PLEASE how can this be done , because I need to make a redirect that automatically identifies the params when making a redirect with Navigate Major question is how to access the :type in Navigate to=""

iamunadike commented 2 years ago

Anyt thoughts on how this can be done with react v6

<Route path={ "/shop/products/:category/:page" } />

<Redirect from="/shop/products/:category"
          to="/shop/products/:category/1" exact={ true } />
Kepro commented 2 years ago

@iamunadike you're in issues, for discussion use "discussion" or stackoverflow

Abdilar commented 2 years ago

Without optional parameters, react-router v5 is better than v6

Kepro commented 2 years ago

@timdorr so are we playing a dead bug? :) lot of downvotes, a lot of developers are in pain because you removed optional parameters without explanation or open discussion...

timdorr commented 2 years ago

I didn't remove them personally. Don't be rude and direct your anger at me. I haven't contributed to the code base in some time.

Kepro commented 2 years ago

@timdorr, sorry but you were the one that wrote "We don't plan on supporting them in v6." without any explanation...

caub commented 2 years ago

@timdorr Can I try some pull-request for supporting them? in the same direction than my previous comment (it would expand all possibilities)

jinspe commented 2 years ago

another hacky solution if you don t want to unmount your components is to use useSearchParams so your url will look like this /params1?p2=params2 you will have to do a bit of logic inside your components with a useEffect on the searchParams this can get kind of ugly but maybe it is easier that way.

zachsiegel-capsida commented 2 years ago

I agree with the frustrated commenters that this removal should absolutely be documented, since it used to be supported, and since many widely-used routers support and have historically supported optional parameters in a first-class way.

Or you could just support them.

Thank you!

caub commented 2 years ago

Here's a good workaround: https://replit.com/@caub/makeOptionalRoutes#index.js

EDIT: I start to understand this decision to not support optional path params, it's more explicit and actually better, I ended up just adding all possible routes in my config (and not using the helper above)

actuallymentor commented 2 years ago

@timdorr Can I try some pull-request for supporting them? in the same direction than my previous comment (it would expand all possibilities)

Not sure how much your bandwidth is, but I think many people would appreciate a pull request for that, and would argue in favor for that functionality.

Not supporting optional parameters is counter industry standards and many of us have headaches because of this choice.

powelski commented 2 years ago

I thought that optional params and dynamic params (ability to match multiple segments as a single param) were the most basic features of any reasonable router library. I was wrong.

zachsiegel-capsida commented 2 years ago

@powelski I wonder whether there's a good reason for this design choice, though. Maybe the idea was that processing route parameters should happen in functional components with their own (sub-) routes, and maybe optional parameters can always be handled in this way.

If anyone can drop some knowledge about this project, I would greatly appreciate it!

powelski commented 2 years ago

@zachsiegel-capsida no idea, but to me it looks like a terrible decision.

alexiuscrow commented 2 years ago

I searched for a way to use the optional path prefix. In my case, it needed to split the routes by locales. Taking into account the changes in the v6, there is a simple scratch with a better solution I found:

import MainContainer from '../MainContainer';
import {Routes, Route} from 'react-router-dom';
import HomePage from '../../pages/Home';
import AboutPage from '../../pages/About';
import NotFoundPage from '../../pages/NotFound';
import {useTranslation} from 'react-i18next';

const Application = () => {
  const {i18n} = useTranslation();
  const langPathPrefixes = [...i18n.languages, ''];

  const langRoutes = langPathPrefixes.map(langPrefix => (
    <Route path={`/${langPrefix}`} element={<MainContainer />} key={`react-router-${langPrefix}`}>
      <Route index element={<HomePage />} />
      <Route path="about" element={<AboutPage />} />
      <Route path="*" element={<NotFoundPage />} />
    </Route>
  ));

  return <Routes>{langRoutes}</Routes>;
};

export default Application;
AndrewEastwood commented 2 years ago

jamalkaksouri

seems more clunky and messy right now ...

peterlau commented 2 years ago

As an OSS maintainer (of a different project) myself I appreciate that when folks are contributing a useful product for free, it's a case of what-you-see-is-what-you-get.

In this instance, I think what's missing from the official response is that "We're not supporting optional params in V6 because nested Routes semantically more flexible".

The 'Whats new' docs do go into some depth about the changes: https://reactrouter.com/docs/en/v6/upgrading/v5#relative-routes-and-links

TruePath commented 2 years ago

So if I have routes like the following (e.g. the replacement for optional parameters) and just assign the same key prop to both won't react treat them as the same component (retaining state and keeping what it can in the DOM)?

        <Routes>
            <Route path="/" element={<BarWrapper />}>
                <Route path="*" element={<DefaultBar />}/>
                <Route path="project/:id/:action?/*" element={<ProjectBar key='projectbar'  />}/>
                <Route path="project/:id/*" element={<ProjectBar key='projectbar' />}/>
            </Route>
        </Routes>

If that's correct then it seems like this is a pretty easy fix. However, I'm not sure enough about react internals to be totally sure what this does and/or how it works with class components.

EDIT: Ok, not a super easy fix if you have an optional parameter below a parameter the key depends on but at least it's a fix.

Ahh, I now see why @alexiuscrow was suggesting the approach he did.

AChristoff commented 2 years ago

I am sorry to say it but v5 is better than v6. Super cool features are gone... like gone!!! and why ??? Instead of adding more and make the old easy to use you guys cut so much of the router..... I mean who does that?!