Skip to content

Rewrite, version 1.0 #1158

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

Merged
merged 153 commits into from
Jun 12, 2015
Merged

Rewrite, version 1.0 #1158

merged 153 commits into from
Jun 12, 2015

Conversation

mjackson
Copy link
Member

@mjackson mjackson commented May 6, 2015

NOTE: DO NOT MERGE YET

This PR is a complete rewrite of the router with the following goals in mind:

  • simpler top-level API with less boilerplate
  • async route config and/or component loading (better support for code splitting in large apps)
  • simpler API for server-side rendering
  • more React-like <RouteHandler> API
  • easier data fetching API

... and a bunch of other stuff that we've learned from various issues over the past year.

Here's a summary of the various things you can do:

Top-level API

var { createRouter, Route } = require('react-router');

var Router = createRouter(
  <Route component={App}>
    <Route name="home" component={Home}/>
  </Route>
);

// The minimal client-side API requires you to pass a history object to your router.
var BrowserHistory = require('react-router/lib/BrowserHistory');
React.render(<Router history={BrowserHistory}/>, document.body);

// On the server, you need to run the request path through the router
// first to figure out what props it needs. This also works well in testing.
Router.match('/the/path', function (error, props) {
  React.renderToString(<Router {...props}/>);
});

The props arg here contains 4 properties:

  • location: the current Location (see below)
  • branch: an array of the routes that are currently active
  • params: the URL params
  • components: an array of components (classes) that are going to be rendered to the page (see below for component loading)

The branch and components are particularly useful for fetching data, so you could do something like this:

BrowserHistory.listen(function (location) {
  Router.match(location, function (error, props) {
    // Fetch data based on what route you're in.
    fetchData(props.branch, function (data) {
      // Use higher-order components to wrap the ones that we're gonna render to the page.
      wrapComponentsWithData(props.components, data);
      React.render(<Router {...props}/>, document.body);
    });
  });
});

Inside your App component (or any component in the hierarchy) you use this.props.children instead of <RouteHandler> to render your child route handler. This eliminates the need for <RouteHandler>, context hacks, and <DefaultRoute> since you can now choose to render something else by just checking this.props.children for truthiness.

<NotFoundRoute> has also been removed in favor of just using <Route path="*">, which does the exact same thing.

Non-JSX Config

You can provide your route configuration using plain JavaScript objects; no JSX required. In fact, all we do is just strip the props from your JSX elements under the hood to get plain objects from them. So JSX is purely sugar API.

var Router = createRouter(
  {
    component: App,
    childRoutes: [{
      name: 'home',
      component: Home
    }]
  }
);

Note the use of childRoutes instead of children above. If you need to load more route config asynchronously, you can provide a getChildRoutes(callback) method. For example, if you were using webpack's code splitting feature you could do:

var Router = createRouter(
  {
    component: App,
    getChildRoutes(callback) {
      require.ensure([ './HomeRoute' ], function (require) {
        callback(null, [ require('./HomeRoute') ]); // args are error, childRoutes
      }
    }
  }
);

Which brings me to my next point ...

Gradual Path Matching

Since we want to be able to load route config on-demand, we can no longer match the deepest route first. Instead, we start at the top of the route hierarchy and traverse downwards until the entire path is consumed by a branch of routes. This works fine in most cases, but it makes it difficult for us to nest absolute paths, obviously.

One solution that @ryanflorence proposed was to let parent routes essentially specify a function that would return true or false depending on whether or not that route thought it could match the path in some grandchild. So, e.g. you would have something like:

var Router = createRouter(
  {
    path: '/home',
    component: Home,
    childRouteCanMatch(path) {
      return (/^\/users\/\d+$/).test(path);
    },

    // keep in mind, these may be loaded asynchronously
    childRoutes: [{
      path: '/users/:userID',
      component: UserProfile
    }]
  }
);

Now, if the path were something like /users/5 the router would know that it should load the child routes of Home because one of them probably matches the path. This hasn't been implemented yet, but I thought I'd mention it here for discussion's sake.

Async Component Loading

Along with on-demand loading of route config, you can also easily load components when they are needed.

var Router = createRouter(
  {
    path: '/home',
    getComponents(callback) {
      require.ensure([ './Home' ], function (require) {
        callback(null, require('./Home')); // args are error, component(s)
      }
    }
  }
);

Rendering Multiple Components

Routes may render a single component (most common) or multiple. Similar to ReactChildren, if components is a single component, this.props.children will be a single element. In order to render multiple components, components should be an object that is keyed with the name of a prop to use for that element.

var App = React.createClass({
  render() {
    var { header, sidebar } = this.props;
    // ...
  }
});

var Router = createRouter(
  {
    path: '/home',
    component: App,
    childRoutes: [{
      path: 'news',
      components: { header: Header, sidebar: Sidebar }
    }]
  }
);

Note: When rendering multiple child components, this.props.children is null. Also, arrays as children are not allowed.

Props

Aside from children, route components also get a few other props:

  • location: the current Location (see below)
  • params: the URL params
  • route: the route object that is rendering that component

Error/Update Handling

The router also accepts onError and onUpdate callbacks that are called when there are errors or when the DOM is updated.

History/Location API

Everything that used to be named *Location is now called *History. A history object is a thing that emits Location objects as the user navigates around the page. A Location object is just a container for the path and the navigationType (i.e. push, replace, or pop).

History objects are also much more powerful now. All have a go(n) implementation, and HTML5History and History (used mainly for testing) have reliable canGo(n) implementations.

There is also a NativeHistory implementation that should work on React Native, tho it's a little tricky to get it working ATM.

Transition Hooks

The willTransitionTo and willTransitionFrom transition hooks have been removed in favor of more fine-grained hooks at both the route and component level. The transition hook signatures are now:

  • route.onLeave(router, nextState)
  • route.onEnter(router, nextState)

Transition hooks still run from the leaf of the branch we're leaving, up to the common parent route, and back down to the leaf of the branch we're entering, in that order. Additionally, component instances may register hook functions that can be used to observe and/or prevent transitions when they need to using the new Transition mixin. Component instance-level hooks run before route hooks.

var { Transition } = require('react-router');

var MyComponent = React.createClass({
  mixins: [ Transition ],
  transitionHook(router) {
    if (this.refs.textInput.getValue() !== '' && prompt('Are you sure?'))
      router.cancelTransition();
  },
  componentDidMount() {
    this.addTransitionHook(this.transitionHook);
  },
  componentWillUnmount() {
    this.removeTransitionHook(this.transitionHook);
  },
  render() {
    return (
      <div>
        <input ref="textInput" type="text"/>
      </div>
    );
  }
});

Anyway, apologies for the long-winded PR, but there's a lot of stuff here! Please keep comments small and scoped to what we're doing here. I'd hate for this to turn into a huge thread :P

Edit: Added data-loading example.
Edit: Added transition hooks.
Edit: Added props for named children, disallow arrays.
Edit: Added addTransitionHook/removeTransitionHook API.
Edit: Renamed Router.run => Router.match
Edit: Removed static transition hooks

Stuff we still need:

Ok. Stuff we still need here:

  • Support absolute paths inside nested UI somehow in new path matching algorithm
  • Move routerWillLeave hook to instance lifecycle method instead of static Add component-level API for observing/preventing transitions
  • Add back scroll history support (@gaearon can you help w/ this?)
  • Maybe remove canGo(n) support (@taurose can you help determine if the current impl. is legit or not? if not, let's just remove it)

COME ON! LET'S GET THIS MERGED AND SHIP 1.0!!!!

@rickharrison
Copy link

In regards to the transition changes, will it be possible to define onChange on a component? Similar to how React couples the markup with logic, I like keeping my component's requirements defined within a component. As an example, certain users have/don't have access to a route based on their account:

willTransitionTo: function (transition) {
  if (!UserStore.getOrganization().supportsThisFeature) {
    transition.redirect('dashboard');
  }
}

@agundermann
Copy link
Contributor

Wow, lots of changes 😄 . My thoughts on this:

  • How/where should async data fetching happen?
  • Do we really need async routes? Seems to me it adds more complexity than it solves. How about resolving links?
  • How does the top level API work server-side with async route, component and data loading? Your example is completely synchronous
  • What happens during async route/component loading? Is there a way to specify placeholders and transition early, i.e. rendering upcoming routes that are already loaded?
  • I don't think HTML5History.canGo(n) can be made reliable..

@ryanflorence
Copy link
Member

@rickharrison in onChange you can walk the prevState.branch and call willTransitionFrom on each, and then walk the nextState.branch and call willTransitionTo. I'd personally like a complete set of backwards compat functions that are all used in a backwards compat Router.run so you can plug in the new code w/o changing your app.

@mjackson
Copy link
Member Author

mjackson commented May 6, 2015

@taurose Thank you for your thoughtful response :) You have some great questions. I'll try to answer them here.

How/where should async data fetching happen?

This is the big question. Honestly, I was trying not to think of the answer to this question and get everything else right before even considering it. I find that if I can narrow down the requirements, a lot of other things become clearer. So ya, the short answer is I don't know yet. ;)

The idea was that anything that needs to be loaded asynchronously could be done inside the onChange hook. That probably means that it needs to be asynchronous. Ideally we would be able to build lots of different things into this hook and provide it as public API for people who want to hook into the router, like Relay.

Do we really need async routes? Seems to me it adds more complexity than it solves. How about resolving links?

Indeed. Asynchronously loading route config is only something that you need on really large apps, and even then only client-side, so we are adding a lot of complexity for a very specific use case. However, this is a use case that almost all of our large scale users have (i.e. Facebook, Twitter) so it's worth solving.

Edit: Just saw the 2nd part of this question. If you need to link to a route that you load asynchronously, you need to use an absolute URL in your <Link to>. Basically <Route name> is useless when you don't know all your routes.

How does the top level API work server-side with async route, component and data loading? Your example is completely synchronous

Discarding data for a sec, the only time we need to asynchronously load route config and components is on the client where we have state. In other words, you can't asynchronously load route config and components on the server (but you shouldn't need to).

Data is another beast entirely. Ideally we would have the same API for loading data on both clients and servers. See above.

What happens during async route/component loading? Is there a way to specify placeholders and transition early, i.e. rendering upcoming routes that are already loaded?

Currently, nothing. I think we probably need to keep the currentTransition in the router component's state (it is state after all) so your render tree knows if it's transitioning. Then at least you could show a spinner or something. But ideally yes, we would have a declarative API for this.

I don't think HTML5History.canGo(n) can be made reliable..

AFAICT there are two things we need to know in order for canGo(n) to be reliable:

  1. How many entries are in the history (i.e. History.length)
  2. What is our current index in the history (i.e. History.current)

With HTML5's pushState(state, ...) API, we just save that data to the DOM on push(path) or replace(path). When the router first loads, we can read that data from window.history.state. When we get a popstate event, we read that data from event.state.

Which part of that isn't reliable?

@mjackson
Copy link
Member Author

mjackson commented May 6, 2015

@taurose One thing I tried my best to do with this is get the push/replace/pop semantics right. You had commented that our use of "pop" wasn't quite right in the past, and I think I know now what you meant by that.

@mjackson
Copy link
Member Author

mjackson commented May 6, 2015

One thought I had with the onChange/onUpdate API was that I'd like it to be a standard interface for anyone who wants to plug into the router. So you could, e.g. do something like:

Edit: I updated the original PR above with a better idea.

// The "transition manager" would be a thing that you could easily plug
// into your router's onChange hook. It could be responsible for managing
// any # of things including fetching data (and cancelling outbound requests
// for data when the routes change mid-flight), updating scroll position, etc.
var TransitionManager = createTransitionManager();

var Router = createRouter({
  routes: ...,
  onChange: TransitionManager.handleRouterChange
});

...

@agundermann
Copy link
Contributor

Thanks for the detailed response.

I still don't quite understand the async routes use case. Why isn't it enough to have async handlers and a decentralized route config?

Which part of that isn't reliable?

I think you shouldn't store .length in history state, since it'll most likely be stale once you get back to it, e.g. going from /a to /b, then pressing back button would reload history.length as 1, even though it's 2. Or am I missing something? 😄

Either way, there's some browser inconsistencies I encountered working on #843, especially related to hash navigation. Clicking a <a href="#something"> can cause state to be reset (in some cases without even firing an event). I think the other big issue was refreshing the page by various means, which could also lead to state being lost.

@rickharrison
Copy link

@ryanflorence As someone who has upgraded with every version, having some backwards compatibility functions would be super helpful and much appreciated! :) Also, I'd be glad to help out with creating those and testing them with my apps. Perhaps we can chat about the approach in IRC sometime.

@vladap
Copy link

vladap commented May 6, 2015

It looks like a great amount of work. I have couple of questions:

  1. I have noticed that examples are rewritten using mixins instead of this.context. Does it suggests that this.context.router is abandoned for 1.0?

  2. Is there a strategy how to access flux stores instances which aren't singletons (isomorphic apps) in transitions? Would I do it in onChange and onUpdate hooks?

  3. Is there any timeframe 1.0 could possibly land? I would like to estimate if 1.0 could be possibly used for our product before our release.

@mjackson
Copy link
Member Author

mjackson commented May 6, 2015

@taurose

I think you shouldn't store .length in history state, since it'll most likely be stale once you get back to it, e.g. going from /a to /b, then pressing back button would reload history.length as 1, even though it's 2. Or am I missing something?

Aaaaaah, you're absolutely right. ;) We can fix that with a getLength() method that reads window.history.length in that case tho, right?

As for random browser breakages, maybe there's no way around those...

@mjackson
Copy link
Member Author

mjackson commented May 6, 2015

@vladap we're gonna ship 1.0 within a week. there, i said it. now we gotta do it ;)

@agundermann
Copy link
Contributor

The problem with window.history.length is that it takes other websites into account as well. So canGo(n) && go(n) could take us out of the app.

That's why, for the PR I mentioned, I opted to use an ID to identify the current visit/session, whose length could then be stored in sessionStorage (or even localStorage). The id would be stored in history state, in addition to current.

Example browser history:

  1. google.com/
  2. myApp.com/, state = { id: 1, current: 1 }
  3. myApp.com/users, state = { id: 1, current: 2 }
  4. myApp.com/users/new, state = { id: 1, current: 3 }
  5. facebook.com/
  6. myApp.com/, state = { id: 2, current: 1 }
  7. myApp.com/contact, state = { id: 2, current: 2 }

sessionHistory: [ { id: 1, length: 3}, { id: 2, length: 2} ]

@edygar
Copy link
Contributor

edygar commented May 6, 2015

Router.run(cb) will still be there, right? So data fetching keeps the same way as suggested by @ryanflorence on react-router-mega-demo, don't ?

@ryanflorence
Copy link
Member

Re data fetching,

Michael and I spent a couple hours this morning on it (and several hours in times past, at his house, at a burger joint, over screen shares), it will be similar to what we do in the callback of Router.run.

@karlmikko
Copy link

Smashing it out of the park again!

@johanneslumpe
Copy link
Contributor

Just to be clear: if we will call our own static methods in onChange, how will we be able to access the current transition object? Will the router pass it as argument or will there be a method on the router like getActiveTransition? Or am I wandering off into the wrong direction here?

@vladap
Copy link

vladap commented May 7, 2015

@mjackson: one week... haha, it is even faster then I can possibly adapt. Thanks for the great work.

@uberllama
Copy link
Contributor

"The willTransitionTo and willTransitionFrom transition hooks have been removed."

This is troubling. We use willTransitionTo extensively for both authentication and role-based authorization. An example below of an Authenticated component, under which all authentication-requiring components are nested.

var Authenticated = React.createClass({

  // Redirect unauthenticated users
  statics: {
    willTransitionTo: function(transition, params, query, next) {
      var currentUser = SessionStore.currentUser();
      if (!currentUser) {
        transition.redirect('login', null, { query: transition.path });
      }
      next();
    }
  },

  render: function() {
    return <RouteHandler/>
  }
}

I may have missed it, but what's the intended replacement for interception in these type of scenarios?

@ryanflorence
Copy link
Member

@uberllama everything you used to be able to do with transitions you will still be able to do, the upgrade guide will have detailed information on how to update your app, and maybe even a backwards compat module.

@uberllama
Copy link
Contributor

@ryanflorence I'll stay tuned. Is there anything ready I can look at in the interim?

@jeffbski
Copy link
Contributor

jeffbski commented May 7, 2015

Looks great, nice work all.

Could you clarify what nested routes look like in the non-jsx form? Would it be something like?

var Router = createRouter(
  {
    component: App,
    childRoutes: [{
      name: 'home',
      component: Home,
      childRoutes: [{
        name: 'profile',
        component: Profile
      }]
    }]
  }
);

@mjackson
Copy link
Member Author

mjackson commented May 7, 2015

@jeffbski ya, you've got it. :)

@jeffbski
Copy link
Contributor

jeffbski commented May 7, 2015

Great, I like it.

state = JSON.parse(value);
} catch (e) {
// Invalid state in AsyncStorage?
state = {};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably at least add a log statement here. If it has an invalid AsyncStorage, the app is probably hosed anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@duncanfinney sure, a log statement is a fine idea. Although, I didn't see console.log anywhere in the React Native docs, so I'm still a little unsure how to do it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.log and console.warn go to the console/Xcode logs; console.error should cause a redbox.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent. Thanks for the info @spicyj !

@ryanflorence
Copy link
Member

WHY YOU PEOPLE AWAKE?

@rmoorman
Copy link

👍

@dashed
Copy link
Contributor

dashed commented Jun 12, 2015

👍 Awesome! Looking forward to see v1.0

@ryanflorence
Copy link
Member

Thanks :) If you're adventurous:

npm install react-router@alpha
https://rackt.github.io/react-router/tags/v1.0.0-alpha1.html

We know we are missing some stuff, a bit more work and we'll have a beta that we will believe to be "complete".

You may find the examples directory to be particularly interesting, might I point you toward huge-apps async-data, and pinterest-style ui?

Also we'll have an upgrade guide with the beta when it comes.

@okcoker
Copy link

okcoker commented Jun 12, 2015

+1

Installing…

@johanneslumpe
Copy link
Contributor

@ryanflorence There is this area called Europe :D

@ryanflorence
Copy link
Member

@johanneslumpe I'd have to see it for myself...

@devknoll
Copy link

WHY YOU PEOPLE AWAKE?

Because someone told us there'd be a react-router beta tonight... 😉

@emmenko
Copy link

emmenko commented Jun 12, 2015

Awesome, congrats!!! 👍

@devknoll
Copy link

This might just be an idiot filter, but [email protected] doesn't seem to contain the compiled sources in /lib

@okcoker
Copy link

okcoker commented Jun 12, 2015

So far I've added the lib/umd/ReactRouter.js path to my webpack resolve aliases as 'react-router' and things built for me. Still working through other errors though.

@ryanflorence
Copy link
Member

alright, I've fixed it, you can

npm install react-router@alpha

and then require it as usual, npm was using .gitignore, which was ignoring our built modules in lib.

@ryancole
Copy link

👍 👍 👍 👍

@devknoll
Copy link

What's the intended way to handle data fetching for index components? My routes look like:

React.render((
  <Router history={BrowserHistory} createElement={createElement}>
    <Route path="/" component={App} indexComponent={Dashboard}>
      <Route name="welcome" path="welcome" component={Welcome} />
    </Route>
  </Router>
), document.body);

My setup is very similar to Relay. My Dashboard component has a query that needs to be executed if we're going to the Dashboard, but that can't happen unless it's created with createElement (so that it can be wrapped in a RootContainer). That is, handling !this.props.children in App wouldn't really work.

It seems like the docs use indexComponent but it doesn't look like it's implemented yet.

@devknoll
Copy link

I'm working around this for now by using indexRoute and making my routes with objects like the pinterest example.

@kawing-chiu
Copy link

I have a question regarding server-side rendering: how do we pass data to the App? I've read through the v1.0.0-beta3 doc and this thread, but could not get it to work. The 'Top-level API' section above mentions the following:

wrapComponentsWithData(props.components, data);

But it's not clear that how is this function supposed to be implemented? Later in this thread mentions componentProps but it seems that it's not present in v1.0.0-beta3? To be clear, what I'm trying to implement is:

// server-side
var routes = (
  <Route path="/" component={App} />
);
var location = new Location('/');

Router.run(routes, location, (err, initialState, transition) => {
  fetchData(url).then((data) => {
    // how to pass *data* so that I can access it in App?
    var html = React.renderToString(
      <Router {...initialState}  />
    );
  });
});

@ariporad
Copy link

ariporad commented Aug 3, 2015

Hey, sorry if I missed this (I did read the thread, I really did), but is there an approximate timeline for react-router 1.0.0?

Thanks!

@dmr
Copy link

dmr commented Aug 4, 2015

I'm also interested in the timeline. There are some great changes in this rewrite and I want to use them :)

@jaraquistain
Copy link

Same. I've been putting off implementing my auth re-direction forever because 1.0 makes it infinitely easier to do. Just want an idea whether or not I actually am going to have to do it the hard way or I can just wait until 1.0 ships

@crohde7707
Copy link

I to am interested in the timeline, really itching for that 1.0.0 stable release ;)

@jaraquistain
Copy link

@ryanflorence @mjackson any rough approximation? +/- a few weeks or a month even?

@matias-sandell
Copy link

+1

@th0r th0r mentioned this pull request Aug 18, 2015
@ghost
Copy link

ghost commented Aug 20, 2015

Will this router be able to render a server side partial html view (that is not a react component) tostring?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.