ponyfoo.com

Universal Routing in React with ES6

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

Yesterday, we learned about how to set up a build process for an universal ES6 React app using Babel, and how to have that React app render “universally” – on both the server-side and the client-side. Today we’ll add routing capabilities to the application so that it isn’t literally a “single-page app” anymore.

One thing there’s to like about React for sure is that it’s only the V in V* (View, Whatever). When it comes to routing in your React app, you could implement it yourself by hand or you could use react-router. Implementing it yourself might sound tempting at first, but react-router makes it easy to expand the already-universal app we have with routing capabilities without having to do much different things in either the server or the browser.

Using react-router

The Express app.js module we built in the last article looks something like this.

import express from 'express';
import hbs from 'express-handlebars';
import React from 'react/addons';
import App from './components/app';

var app = express();
app.engine('html', hbs({ extname: 'html' }));
app.set('view engine', 'html');
app.locals.settings['x-powered-by'] = false;
app.get('/', function home (req, res, next) {
  res.render('layout', {
    reactHtml: React.renderToString(<App />)
  });
});
app.listen(process.env.PORT || 3000);

If we wanted to add more routes, we’d have to add more statements like app.get('/', function...) above, as well as load each component necessary to render every one fo those routes. As your application grows, complexity would grow linearly as well. A better alternative is using react-router, which allows you to remove Express from the equation and leave routing to the React application itself. Let’s install react-router via npm.

npm i react-router -S

Then, you’ll need to import the module into your app.js file.

import Router from 'react-router';

We’ll now defer routing to react-router instead of routing at the Express level, using app.get and the like. If you’re following along in code, get rid of the app.get('/', function...) piece, and also slash the import statement for App. We’re going to implement a middleware method for Express that will handle routing on its behalf.

app.use(router);

The router method is displayed below. It creates a routing context using react-router and the request’s req.url. react-router will figure out the component that should be rendered for that particular location, and we’ll get that back as a Handler. We can then leverage JSX to render the <Handler /> using React.renderToString.

function router (req, res, next) {
  var context = {
    routes: routes, location: req.url
  };
  Router.create(context).run(function ran (Handler, state) {
    res.render('layout', {
      reactHtml: React.renderToString(<Handler />)
    });
  });
}

* More on these in a moment!

Note how this method is component-agnostic, as it should be able to figure out what component to render based on all the routes that we have and the location the human is trying to visit. This helps us decouple the web application from our React components for good.

Defining Your Routes

How does the routes object look like? It should be a module, because you’ll also be leveraging the exact same module in your client-side code so that routing stays consistent. Fair enough, let’s add the import statement.

import routes from './routes';

How should the module actually look like? Something like this, maybe? As you can see, the route definitions for react-router leverage JSX to declare a nested route hierarchy. We only have one route, though.

import React from 'react';
import {Route} from 'react-router';
import HomeIndex from './components/home/index';

export default (
  <Route path='/' handler={HomeIndex}>
  </Route>
);

Except that, most of the time, you’ll have pieces of your app outside of routing – your navigation, sidebars and whatnot. In order to future-proof, you’re better off defining an <App /> component as well. Consider the following mockup from the react-router documentation, which illustrates the point well enough.

A mockup of an application with a top navigation bar and a dashboard
A mockup of an application with a top navigation bar and a dashboard

It’ll be useful to wire up an <App /> component where we can at a later point add some navigation elements. Let’s start with the changes to the router.js module. You just need to also import the components/app component, and change the routing definition.

import React from 'react';
import {Route, DefaultRoute} from 'react-router';
import App from './components/app';
import HomeIndex from './components/home/index';

export default (
  <Route path='/' handler={App}>
    <DefaultRoute handler={HomeIndex} />
  </Route>
);

The <DefaultRoute /> is used when the parent route’s path is matched exactly. So when the human navigates to /, <HomeIndex /> will be rendered.

Now that our routing is wrapped within the <App /> component, you can place shared functionality and markup in that component (like we said earlier – navigation and whatnot). For the time being though, our components/app.js file is almost an empty component. The relevant code is highlighted.

import React from 'react'
import {RouteHandler} from 'react-router';

export default class App extends React.Component {
  render () {
    return <RouteHandler />
  }
};

You can think of <RouteHandler /> as “nesting continues here”. In other words, whenever you have a React component rendered using react-router, it’ll be rendered at <RouteHandler /> in its parent route’s component. See the documentation if I lost you on that one.

Continuing with the example about a human visiting /, the react-router will render {HomeIndex}, and then jump to the parent route. The parent has an {App} handler, so it’ll render that and place the result of rendering {HomeIndex} inside {App}'s <RouteHandler />. It’s very straightforward and subtly powerful.

React’s react-router was modeled after the often-praised Ember router. You can nest your routes as deep as you need to, and you can mix in as many components as needed too. All your routing needs are now covered by routes.js.

But, wait! What about client-side routing to match?

There isn’t much else that needs to be done on the client-side to match your server-side react-router routes. Let’s go back to what we used to have. As you probably guessed, references to your old <App /> will now be removed in favor of our newfound routing capabilities.

import React from 'react/addons';
import App from '../../components/app';
var main = document.getElementsByTagName('main')[0];

React.render(<App />, main);

We’ll be once again pulling in the react-router as well as reusing the routes.js module we’ve defined for the server-side, one directory up. Instead of rendering <App /> directly like we used to, we’re going to defer to the wisdom of react-router to tell us what component should be rendered. You can specify whether you want the router to work through hashes like #/foo/bar (the default), or via the history API – by specifying the use of Router.HistoryLocation explicitly.

import React from 'react/addons';
import Router from 'react-router';
import routes from '../routes';
var main = document.getElementsByTagName('main')[0];

Router.run(routes, Router.HistoryLocation, function ran (Handler, state) {
  React.render(<Handler />, main);
});

We’re done. You should now understand how to handle routing in your brand new React app, how to get that working on both the server-side and the client-side without having to make changes in multiple places, and how to leverage nesting so that you can add some navigation or layout to your app from within a React component.

This is fun stuff!

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments