The marmelab blog

Custom Route Component For React Router

Published on 20 September 2016 by François Zaninotto with tags ReactJS

In complex React.js applications, I sometimes see groups of pages following the same routing pattern. For instance, for an application doing mainly Create, Retrieve, Update, and Delete (CRUD) operations on posts and comments, the expected routes may look like:

posts/              // list posts (Retrieve)
posts/create        // Create a post
posts/:id           // Edit a post
posts/:id/remove    // Delete a post
comments/           // List comments
comments/create     // create a comment
comments/:id        // Edit a comment
comments/:id/remove // remove a comment

Using react-router v3, I’d use the following configuration to implement these routes:

<Route path="posts">
    <IndexRoute component={PostList} />
    <Route path="create" component={PostCreate} />
    <Route path=":id" component={PostEdit} />
    <Route path=":id/remove" component={PostRemove} />
</Route>
<Route path="comments">
    <IndexRoute component={CommentList} />
    <Route path="create" component={CommentCreate} />
    <Route path=":id" component={CommentEdit} />
    <Route path=":id/remove" component={CommentRemove} />
</Route>

You can immediately spot the repetition. Wouldn’t it be nice if I could write the following instead?

<CrudRoute path="posts" list={PostList} create={PostCreate} edit={PostEdit} remove={PostRemove} />
<CrudRoute path="comments" list={CommentList} create={CommentCreate} edit={CommentEdit} remove={CommentRemove} />

…and put all the common routing logic in a custom <CrudRoute> component?

It turns out it’s surprisingly difficult, and it’s due to the nature of the <Route> component. It’s not a real React component, it doesn’t use the render() method, or any of the lifecycle callbacks. The react router documentation doesn’t provide a way to create custom route components, but there is one undocumented feature allowing it.

Without further ado, let’s see the working code for this <CrudRoute> component:

import React from 'react';
import { IndexRoute, Route } from 'react-router';
import { createRoutesFromReactChildren } from 'react-router/lib//RouteUtils';

const CrudRoute = () => <div>&lt;CrudRoute&gt; elements are for configuration only and should not be rendered</div>;

CrudRoute.createRouteFromReactElement = (element, parentRoute) => {
    const { path, list, edit, create, remove } = element.props;
    // dynamically add crud routes
    const crudRoute = createRoutesFromReactChildren(
        <Route path={path}>
            <IndexRoute component={list} />
            <Route path="create" component={create} />
            <Route path=":id" component={edit} />
            <Route path=":id/remove" component={remove} />
        </Route>,
        parentRoute
    )[0];
    // higher-order component to pass path as resource to components
    crudRoute.component = ({ children }) => (
        <div>
            {React.Children.map(children, child => React.cloneElement(child, { path }))}
        </div>
    );
    return crudRoute;
};

export default CrudRoute;

As you can see, the main CrudRoute function is not supposed to be used at all. But since we’re writing a React component, we need one.

The real entry point is createRouteFromReactElement. React-router calls this method with a React element as parameter (in our case, the element resulting from a <CrudRoute> component) and it should return a route object. The script builds this route object using createRoutesFromReactChildren, an utility function exposed indirectly by react-router (that’s why it’s buried deeper in the router package in react-router/lib//RouteUtils). It takes a <Route> component, and returns a route object.

Before returning it, the script updates the component prop of the route. This is the prop supposed to contain the Component to render (as in <Route path="foo" component={Foo} />). It’s replaced by a higher order component with the only purpose of injecting the path to all child components. So in the previous example example, PostList will receive "posts" as its path prop.

And that’s all it takes to write a custom route with react-router v3. This can feel a bit hacky, but that’s because react-router itself is a bit hacky. Fortunately, react-router v4 is around the corner, introducing routing via real React components. In the near future, custom routes will become as natural as writing the JSX you want.

Until then, the createRouteFromReactElement technique will help you keep D.R.Y with routing in React applications. We’ve succesfully used it in admin-on-rest, the frontend framework for building admin SPAs on top of REST services. I hope you like it!

comments powered by Disqus