remix-run / react-router

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

Support for dynamic routes #243

Closed ryanatkn closed 10 years ago

ryanatkn commented 10 years ago

Is dynamically creating routes based on application state a supported or planned feature? This jsfiddle shows an example of props being resolved only the first time. Perhaps there's a better pattern I haven't found.

The use case I'm after is explained in the docs for react-router-component, which supports dynamic routes:

Having routes defined as a part of your component hierarchy allows to dynamically reconfigure routing based on your application state. For example you can return a different set of allowed locations for anonymous and signed-in users.

ryanflorence commented 10 years ago

We believe that you should declare all your routes up front, and then validate the visitor's ability to visit them like the auth-flow example.

I'm sure you could setup/teardown some initial routes, make your decision, and then render some new ones though.

ryanatkn commented 10 years ago

@rpflorence Do you have a recommendation for the following use case?

<Route handler={App}>
  <Route name="session-user-page" path={"/" + sessionUser.name} handler={SessionUserPage}/>
  <Route name="public-user-page" path="/:userName" handler={PublicUserPage}/>
</Route>

I could change it to this -

<Route handler={App}>
  <Route name="user-page" path="/:userName" handler={UserPage}/>
</Route>
// `UserPage` matches `sessionUser` against the route param and loads either `SessionUserPage` or `PublicUserPage`

but then the App would give every handler the sessionUser prop whether it uses it or not - which doesn't seem problematic, just not very clean. The alternative, hooking all handlers that need the sessionUser into the store, would be a lot of code duplication.

Thanks for the help!

ryanflorence commented 10 years ago
<Route handler={App}>
  <Route name="session-user-page" path={"/" + sessionUser.name} handler={SessionUserPage}/>
  <Route name="public-user-page" path="/:userName" handler={PublicUserPage}/>
</Route>

That will work fine, as long as you always have a value in sessionUser.name.

I'd personally rather have one route and check a session store to branch in UserPage

but then the App would give every handler the sessionUser prop

Not sure I understand why you think this is the case. Any component can talk to a session store without passing it down through props.

var moduleThatKnowsTheSession = require('../stores/session');
var UserPage = React.createClass({
  // some code to get state from the module that knows the session
  render: function() {
    if (!this.state.session.loggedIn) {
      return <PublicUser/>
    } else {
      return <SessionUser/>
    }
  }
});
module.exports = UserPage;
ryanatkn commented 10 years ago

The problem I ran into with that bit of code not working is that props are only resolved the first render (as shown in the jsfiddle), so passing the sessionUser as a prop meant it never got updated in the handler, even after re-rendering the routes. So I was unable to tear down and re-render routes like you suggested - although it's likely I'm missing something.

I assumed that passing thesessionUser as a prop would be closer to the recommended React best practices. I wanted to avoid setting up multiple store listeners throughout the component hierarchy, instead opting for a small number of "ControllerViews" that the React devs have talked about, like the top-level App that would listen to the session store. Nested listeners ended up causing unmounted components to cause invariants on store changes, but maybe that's the best pattern and I need to check mounted status. It seems like an anti-pattern to forego nested store listeners, directly access store state, and rely on the hierarchy to propagate changes, therefore I came back around to the idea of passing props. There are a lot of possible solutions but at the moment none seem very elegant to me. This is a problem that extends beyond react-router, but it first hit me when trying to pass props to routes based on application state, which I couldn't make work as shown in the jsfiddle.

ryanflorence commented 10 years ago

If your session data is going to change, then yeah, that won't work.

Finally, server apps don't have dynamic routes, not sure why we'd expect a client app to.

ryanatkn commented 10 years ago

Makes sense - thanks for the help!

It looks like my problem with the nested listeners is due to the inability to remove listeners during an emit call in node's EventEmitter implementation, but that seems like reasonable behavior, so I'll just check for isMounted. I'm going to try having nested components listen for changes as described here.

  1. Now all your data is in stores, but how do you get it into the specific component that needs it? We started with large top level components which pull all the data needed for their children, and pass it down through props. This leads to a lot of cruft and irrelevant code in the intermediate components. What we settled on, for the most part, is components declaring and fetching the data they need themselves, except for some small, more generic components. Since most of our data is fetched asynchronously and cached, we've created mixins that make it easy to declare which data your component needs, and hook the fetching and listening for updates into the lifecycle methods (componentWillMount, etc).
ryanflorence commented 10 years ago

Yeah, that's how I do it :)

zackfern commented 10 years ago

@rpflorence I was wondering if you would have any suggestions for creating Routers that are based off of other files that are required at runtime? I'm working on a project which will be a platform that future developers will create plugins to run on, so they need to be able to create their React components, and then register them with the Router. This makes it so there won't be an opportunity for us to declare all routes up front, as you mentioned earlier.

My current thought process is that each component that will be providing a Route "registers" into an object and then we iterate over those registers routes when creating the object. Unfortunately I haven't had any luck so far. Any suggestions?

ryanflorence commented 10 years ago

you can "export" some routes from a plugin maybe?

<Routes>
  {SomePlugin.routes()}
</Routes>
emmenko commented 9 years ago

@zackfern any luck on your problem? I also need to implement some similar solution for a plugin system and I am also struggling with the setup. If you could shred some light or if you found some solution it would be awesome, thanks ;)

revolunet commented 9 years ago

hey :) i'd like to handle infinite nested states in my app, for example i open a "card" and another one can open inside it, and on and on, on-demand. So all these states cannot be predicted, they depend on the user interactions, ex :

technology > javascript > web > css...

any idea how to implement this in a clean way ?

pawawat commented 9 years ago

@zackfern have you got a solution on your problem? I have the similar problem, thanks

ryanflorence commented 9 years ago

use a wild-card path, and then parse it yourself.

this.props.params.splat And then nest the Card view over and over for your parsed path. On Tue, May 19, 2015 at 10:20 AM, pawawat notifications@github.com wrote: > @zackfern https://github.com/zackfern have you got a solution on your > problem? I have the similar problem, thanks > > — > Reply to this email directly or view it on GitHub > https://github.com/rackt/react-router/issues/243#issuecomment-103576129.
zackfern commented 9 years ago

@pawawat I ended up creating my own registration system for registration into the routes. When it came time to generate the Route JSX I looped over my registration array and generated the Route object on the fly.

revolunet commented 9 years ago

@zackfern any code sample ?

vcarl commented 9 years ago

I'm in a weird spot where I'd love support for this, but only so that we can phase it out little by little in the existing application. We're using backbone routes, with subroutes being dynamically added when a route is loaded--which is super gross. I'm pushing for moving to react-router, but having to migrate all at once makes the task a lot more imposing.

bishopZ commented 9 years ago

I too have this issue where my routes are stored in an immutable map. Even converting the map to an array, react-router still does not resolve the routes.

ReactDOM.render(
  <Router history={history}>
      {state.pages.toSeq().toArray().map((item) =>
        <Route 
          key={item.get('route')} 
          path={item.get('route')} 
          component={require(item.get('component'))} />
      )}
  </Router>
  , target
);

I think that using the "*" route and doing our own custom parsing will work for us in the short-term, but another solution would be better. Any help appreciated.

rhzs commented 8 years ago

any update on @bishopZ code? i also can't do the dynamic routes with map.

10thfloor commented 8 years ago

Adding dynamic routes using map would be a great feature. The comment about server-side applications not supporting dynamic routes seems a bit dismissive; there is no reason I can see for enforcing the same paradigm in browser apps.

Use case: I'm building a quiz app where each question gets a route (on client), and users can add questions (by pushing an object into an array), thereby (in a perfect world) adding routes to access those questions, via map ... same need as @bishopZ

What are the drawbacks I'm not seeing? Would this be difficult to implement?

kanedaki commented 8 years ago

Dynamic routes would be also very helpful when loading async modules that also have some routes.

taion commented 8 years ago

Dynamic routes are supported for the async case. You just can't change your root route ATM.

eloiqs commented 8 years ago

I had a problem while trying to generate routes based on my state. I'm dumping that here in case someone stumble upon this post like I did earlier.

const routes = [
  this.state.routes.map(
    route => {
        {
          path: route.path,
          component: route.component,
        }
    }
  )
]

and later when you declare your router:

<Router routes={routes} history={browserHistory} />

from the docs