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.
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!
Comments