Skip to content

Instantly share code, notes, and snippets.

@pluma
Last active November 30, 2015 19:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pluma/03644ef0d5a163f4bf8d to your computer and use it in GitHub Desktop.
Save pluma/03644ef0d5a163f4bf8d to your computer and use it in GitHub Desktop.
Dreamcode for a nested universal/isomorphic router with support for async prefetching and name-based URL generation
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import {createHistory} from 'history';
import {Router} from '?';
import {Home, UserList, UserDetail, Admin, AdminDashboard} from './views';
import {ErrorView, NotFound, Forbidden, Login} from './views/error';
import {createStore} from './store';
// routes are defined as plain old objects
const routes = {
// if resolve is a thunk, drill down into the children
// then invoke the thunk's function with the matching child's result
resolve: () => view => <Provider children={view}/>,
children: [
{
// index routes have the path "/"
path: '/',
name: 'index',
// if resolve is not a thunk, we have a terminal result
// for the route
resolve: () => <Home/>
},
{
// for non-index routes the leading slash is optional
path: 'users',
// name is what we use to look this route up
name: 'users',
children: [
{
// index routes don't have to have their own name
path: '/',
resolve: () => <UserList/>
},
{
// this route has a parameter in its path
path: ':id',
// this route is called "users.detail"
name: 'detail',
// here we can do type-checks/conversions
// if the conversion fails, router tries the next matching route
// also the conversion result can be async (return a promise)
params: {id: value => Number(value)},
// the validated params are available to the resolve function
resolve: ({params}) => <UserDetail id={params.id}/>
}
]
},
{
path: 'admin',
name: 'admin',
// if the resolve function rejects, the router bails
// also the resolve function can return a promise
resolve: ({context}) => context.getState().isAdmin
? view => <Admin children={view}/>
: Promise.reject(403),
// "children" is only entered if the above resolve doesn't reject
children: [
{
path: '/',
resolve: () => <AdminDashboard/>
}
]
},
{
// names and paths don't have to be nested
path: 'i/am/special',
name: 'users.special',
resolve: () => <UserDetail id="special"/>
}
]
};
// Usage example
const store = createStore();
const stage = document.getElementById('stage');
const history = createHistory();
const router = Router(routes);
// the "cancel" logic guarantees we don't
// step on our own feet when the URL changes
// before the route has been resolved
let cancel = () => null;
history.listen(location => {
cancel();
let cancelled = false;
cancel = () => {
cancelled = true;
};
// we pass in the redux store as the "context" of
// the router (so the router itself remains stateless)
router.resolve(location.pathname, store)
.catch(err => {
// routing errors are trivial
if (err === 404) return <NotFound/>;
if (err === 403) return <Forbidden/>;
if (err === 401) return <Login/>;
return <ErrorView error={err}/>;
})
.then(view => {
if (cancelled) return;
ReactDOM.render(view, stage);
});
});
@pluma
Copy link
Author

pluma commented Nov 30, 2015

As suggested, this shouldn't implement a react-router style async getChildRoutes ("dynamic child routes"). That makes reversing of named routes a huge headache and the use case of async loading of bundles with their own routes is just too esoteric (and not trivial to get right for server-side execution).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment