Universal JavaScript Apps with React Router 4

How to use the latest version of React Router both on the server side and the client side

React Router is the most popular library in React-land for rendering different page contents depending on request URLs and manipulating the browser history to keep the URL displayed in the location bar in sync with your app as the user interacts with the page.

Shiny and New

Recently, version 4 of React Router entered the beta release phase. Bemoaned by some, applauded by others, it is a complete rewrite of the previous version with lots of breaking API changes.

The key idea behind version 4 is “declarative composability” — it embraces the component concept that makes React so great and applies it to routing. Every part of React Router 4 is a React component: Router, Route, Link, etc.

One of the React Router developers, Ryan Florence, made a short hands-on video introduction to the latest React Router, which I highly recommend:

What About the Backend?

The new version of React Router comes with a new web page that has lots of useful code examples. One thing I miss, however, is a practical example on how to use React Router for rendering React-based pages on the server side.

For the project I’m currently working on, search-engine friendliness and optimal site speed are essential, so rendering the whole page on the client side — the way all the examples on the examples page do — is not feasible. We use an Express server to render our React pages in the backend.

In his intro video, Ryan has an App component that fetches data from some API to initialize its state, using the componentDidMount lifecycle method. When the asychronous data fetching is done, the component is updated to display that data.

But this doesn’t work when rendering the App component on the server side: when you use renderToString, the string with HTML code is created synchronously, after calling the component’s render method once. componentDidMount is never called.

So if we rendered the App component from Ryan’s video example in the backend, it will just generate the “Loading…” message.

I struggled with this for some time and complained about it on Twitter:

Thankfully, Ryan replied to my tweet and pointed me in the right direction:

The Solution

As a proof of concept, I created a demo app that basically recreates Ryan’s example from the video using server-side rendering.

The app fetches data about Gist code snippets using the GitHub API:

It turns out that Gists are mainly used for promoting counterfeit prescription meds ☺︎

Show Me the Code!

You can find the demo app’s source code here on GitHub:

In a nutshell, here’s what I did…

server/index.js

This is the code that is run with every HTTP request to the Express server:

(Note: this is just an excerpt — you can find the full source code on GitHub)

In lines 1–4, I’m defining an array of routes for my app. The first one is for initial requests for the main page, without any Gists selected. The second route is for displaying a selected Gist.

In line 6, my Express app is told to handle any request that comes in using an asterisk match.

In line 7, I’m reducing my routes array using the matchPath function from React Router; the result is a match object with information about the matching route and any parameters that may be parsed from the URL path.

In lines 8–11, if there is no matching route, I’m rendering an error page that says: “Page not found”.

The render function here is just a wrapper around React’s renderToString that adds the basic page HTML code around the React component’s HTML (<html>, <head>, <body>, etc.).

In lines 12–22, I’m fetching the data to populate my App’s state from the GitHub API and rendering my App component.

Most notably, in line 17 I’m using the StaticRouter component to initialize React Router. This Router component type is the best choice for server-side rendering. It never changes its location, which is what we want in this case, since on the backend, we are just rendering once and not directly reacting to user interations.

Line 23 catches any errors that accur during the process to render an error page, instead.

App.js

My App component looks like this:

(→ full source code on GitHub)

In line 1, the component receives the gists data object as a prop.

Lines 3–13 render a Sidebar component with links to the various Gists. The SidebarItem components contained within are only rendered if there is actually gist data available. On the server, this is always the case. We are, however, using this component for both server-side and client-side rendering. If the component is rendered in the client, we may be in the process of fetching fresh gist data, so we display a “Loading…” message instead.

Line 15 uses a Route component from the React Router library to display the Home component when the route matches the path “/”. We are using an exact match here, otherwise any path that simply starts with a slash would match.

If there is some gist data to display, in line 18, another Route component is used to display a Gist component with details about a selected gist.

client/index.js

As mentioned above, this is a universal JavaScript application (f.k.a. “isomorphic”), meaning the same code is used to render pages on the server and on the client. Here is an excerpt from the code that initializes the page on the client side:

(→ full source code on GitHub)

Much simpler than the server-side version! The render function in line 1 is just the render function of ReactDOM. It attaches the layout rendered by my React components to a DOM node.

In line 2, I’m now using the BrowserRouter (instead of the StaticRouter I used for server-side rendering).

Instead of fetching the initial data from the GitHub API, in line 3 I’m instantiating my App component with gist data from a global variable in the browser DOM, which the backend put there via a <script> tag.

That’s basically it!

When I open up my app in the browser, I can click on any of the Gists in the sidebar. The client-side React Router makes sure that with each click on a link, the page’s URL is updated and the parts of the page dependent on the new URL are re-rendered. When I hit the browser’s reload button, the backend’s static router makes sure that the same page with the correct data is displayed.