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
- [x] ~~Move routerWillLeave hook to instance lifecycle method instead of static~~ Add component-level API for observing/preventing transitions
- [x] 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!!!!