Server-side rendering, code-splitting, and hot reloading with React Router v4

Apostolos Tsakpinis
11 min readSep 24, 2016

--

** WARNING: OUTDATED CONTENT **

This post was based on an alpha build of react-router v4. The final build contained some breaking changes to the API. Documentation can be found here: https://reacttraining.com/react-router/web/

System.import() has been deprecated in favor of import(). Webpack 2+ also supports “magic comments” which simplifies route chunk detection.

For route loading with code splitting (and synchronous rendering on the server), take a look at James Kyle’s excellent react-loadable.

Use of NormalModuleReplacementPlugin for HMR is not really required. The whole thing can be simplified by calling require() for each route module when in development mode. Example can be found here: https://github.com/LWJGL/lwjgl3-www/blob/master/client/routes/index.js

If we had a way to specify webpackMode=eager in our webpack configuration it would simplify things even more. Fingers crossed for this issue: https://github.com/webpack/webpack/issues/5029

React Router v4 has caused a lot of controversy, especially with people that have already invested a lot of effort on the previous versions. I was skeptical at first too, but what won me over was this talk by Michael Jackson and Ryan Florence:

You should definitely watch it you have the time. It made me realize that declarative routing is the way forward, even if we don’t yet have the answers to many problems. Problems like server-side rendering, code splitting (async routes), scrolling, data loading, etc.

Answers for some of those problems are going to be provided by react-router itself, by other libraries, or by code patterns that need to be discovered.

Below is my attempt to tackle some of those:

TL;DR — Just show me the results

The following write-up is based on a website refactored to use React Router v4. You’ll find it at lwjgl.org, a “product website” for the open source library with the same name. Source code of the entire site is available on GitHub:

The migration from the previous version can be seen in a single squashed commit:

There is a lot going on and contains many unrelated changes, so let’s examine the important bits in detail.

What were the requirements?

A lot of effort was invested in the previous version. Although this was just me helping a bunch of Java people with their website, it was a great opportunity to apply new techniques, experiment, discover, and learn things that I could later apply at work.

However, I wasn’t ready to sacrifice any of the following:

1. Server-side route rendering

2. Lazy loading of routes

3. Hot reloading

4. Single rendering path for server & client

What were you using before?

That’s a good question. It is important because many devs will be on a similar migration path with the one faced here. The previous website used:

  • react-router v3 with plain routes configuration
  • react-transform-hmr for hot reloading
  • webpack 2
  • node 6

The route configuration used getComponent() which in turn called System.import() to programmatically import the actual route modules. System.import is natively supported in webpack 2 and works like require.ensure it defines a split point. You can view the full previous configuration here.

You might think that the module names could have easily been parametric and refactored to avoid repetition in the route configuration, but remember, webpack needs to be able to statically analyze the dependency tree for each route (module). Otherwise, you end up with fewer split points and huge bundles! You can read more about code splitting here.

What’s the problem with System.import() and server-side rendering?

Remember the 4th requirement? Well, System.import is not supported in Node. Not even in v6.x. Until the ES6 module situation clears up, we are not going to see native ES6 module support any time soon.

So how did you make it work?

Node was loaded with a babel plugin called system-import-transformer that transformed asynchronous System.import() to synchronous require() calls. That helped Node import the entire dependency tree and since react-router’s match() used callbacks, route callbacks were called naturally and everything worked beautifully.

What was the problem with react-transform-hmr?

Hot reloading with this configuration worked great. Even on the lazy loaded routes. Yes, it had a couple of important issues, but not deal breakers:

  • No hot reloading for functional components
  • Problems with high order components

React Hot Loader 3 solves these problems and will be better going forward, but it was impossible to get it to work with code-split routes.

But react-router v4 examples are full of functional components!

Yes, and they allow for some elegant and powerful stuff. Migration to react-hot-loader 3 was becoming inevitable. But did that mean that code-splitting was no longer possible?

No imperative API on react-router v4 either, therefore no callbacks!

Exactly! So forget about waiting for the code-split route to load before rendering server-side. One more problem to solve.

Server-side Rendering, Code-Splitting, or Hot Reloading, *pick two*.

Or not necessarily? First let’s see some code. We’ll start with the client-side:

only the important bits are shown below, visit the repo for the full code.

Entry Point

Standard react-hot-loader stuff. This is also our entry point on webpack.config.js (that means JavaScript generated for the client will start from here).

import React from 'react'
import {render} from 'react-dom'
import {AppContainer} from 'react-hot-loader'
import App from './ui/App'
const rootEl = document.getElementById('lwjgl-app');render((
<AppContainer>
<App />
</AppContainer>
), rootEl);

if ( module.hot ) {
module.hot.accept('./ui/App', () => {
render(
<AppContainer>
<App />
</AppContainer>,
rootEl
);
});
}

Show me the App

It’s time to mount React Router v4! We import ./Layout which contains the actual website’s interface and content. Notice that we pass the location prop, we’ll need that later.

import React from 'react'
import {BrowserRouter} from 'react-router'
import Layout from './Layout'

const App = () => (
<BrowserRouter>
{
({ location }) => <Layout location={location} />
}
</BrowserRouter>
);

export default App;

Why this indirection? Why don’t you render the UI directly?

Notice the word Browser in BrowserRouter? It is NOT supposed to be used on the server. On Node we’ll wrap Layout in ServerRouter, a special component for the job. Server-side rendering means generating an HTML string, we are not rendering an actual “App”. We want to get the App’s layout in markup form, hence <Layout />. You can call it UI, Page, whatever, just don’t call it App.

Let’s do some routing then

Two important things here that will come in handy. Instead of importing routes one by one we do import * from our routes module. In addition, we wrap routes in a #div that we can reference later.

If you’d like to know more about <Match /> and how routing works in general, you can visit React Router’s excellent website. It has many great examples which use an interactive virtual in-page browser.

import * as Routes from '../routes/Routes'const Layout = props => {

return (
<div id="lwjgl-routes">
<Match exactly={true} pattern="/" component={Routes.Home} />
<Match exactly={true} pattern="/download" component={Routes.Download} />
<Match exactly={true} pattern="/guide" component={Routes.Guide} />
<Match exactly={true} pattern="/source" component={Routes.Source} />
<Match exactly={true} pattern="/license" component={Routes.License} />
</div>
)
};

Outside your <Match/>s you can return top-level components that don’t change for each route, your header and footer, your navigation, etc.
You also have access to props.location if you need it.

How do you define code-split routes?

Heavily relying on quick and dirty code splitting by Andrew Clark, we generate a high-order component for each route.

This is our ./Route.js

function asyncRoute(getComponent) {
return class AsyncComponent extends React.Component {
static Component = null;
mounted = false;

state = {
Component: AsyncComponent.Component
};

componentWillMount() {
if ( this.state.Component === null ) {
getComponent().then(m => m.default).then(Component => {
AsyncComponent.Component = Component;
if ( this.mounted ) {
this.setState({Component});
}
})
}
}

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

render() {
const {Component} = this.state;

if ( Component !== null ) {
return <Component {...this.props} />
}
return null; // or <div /> with a loading spinner, etc..
}
}
}

export const Home = asyncRoute(() => System.import('./home'));
export const Download = asyncRoute(() => System.import('./download'));
export const Guide = asyncRoute(() => System.import('./guide'));
export const Source = asyncRoute(() => System.import('./source'));
export const License = asyncRoute(() => System.import('./license'));

Two import things to notice here:

  1. We still need to pass the relative path of each module in order for webpack’s static analysis to work correctly.
  2. It is critical to check if we are still mounted before calling setState(). The Promise from System.import() will be resolved immediately if the module is already in memory, or just a few ticks later if the JS chunk is cached, but it could take up to several seconds in a slow connection if it has to be loaded from the network. In the meantime, the user might click on some other <Link />. This will result in the route to be unmounted! You wouldn’t call setState on an unmounted component would you?

This behavior is great, and one more reason why React Router v4 is on the right path. It keeps you in a React mindset. You don’t have to think twice about it. It comes natural.

There was no mounted check initially. When React complained about setState, I didn’t have to know anything about react-router or its internals to solve the problem. Heck, I bet most React developers have faced a similar issue before on things completely unrelated to routing.

Router’s API is React’s lifecycle hooks. Beautiful isn’t it?

What else can I do here?

componentWillMount() is a good place to do some prep work. In LWJGL we start showing a Youtube-style progress bar (nprogress) and then hide it as soon as the Promise is resolved. We also do pageview tracking every time a route is fully rendered.

You could even do some parallel data loading, although I feel this is the job of the route itself. I believe routing and business logic should be completely uncoupled.

In render() we can choose to show a loading animation.

In LWJGL, we have a small delay before showing the spinner animation because that improves perceived performance for users with fast connections. To see what it looks like, enable throttling in dev tools and set it to GPRS.

Server-side rendering

Let’s switch to the server. You can find a complete example in React Router’s documention, so the important thing to remember here is to use your UI container component (<Layout/> in our example). Not your webpack entry point, nor your root (<App/>) component that contains BrowserRouter. Otherwise, it won’t work.

import React from 'react'
import {renderToString} from 'react-dom/server'
import { ServerRouter, createServerRenderContext } from 'react-router'
import Layout from '../client/app/ui/Layout'
app.get('*', (req, res, next) => { const context = createServerRenderContext();
const html = renderToString(
<ServerRouter
location={req.url}
context={context}
>
{({ location }) => <Layout location={location} />}
</ServerRouter>
)
));

res.render('index', {html});
});

Wait a minute, didn’t you say Node doesn’t understand System.import?

That’s the least of our problems. Our lazy loaded route components now render in two phases. First they render a blank/loading element and then wait for the Promise to be resolved to render again. There is absolutely no way for the server-side render code to reference and wait on that Promise.

Also, what about React Hot Loader? Isn’t it blind to async routes?

It is.

OK, then, let’s disable code-splitting and try to fix server-side rendering and hot reloading.

We rename our code-split Routes.js module to RoutesAsync.js and leave it on the side.

We create a new Routes.js module with the following content:

export { default as Home } from './home'
export { default as Download } from './download'
export { default as Guide } from './guide'
export { default as Source } from './source'
export { default as License } from './license'

And viola! Server-side rendering now works fine and we also have hot reloading on everything.

So, to recap. During development we want synchronous loading on both server and client. In production we want synchronous loading on the server but asynchronous loading on the client.

What if there was a way to produce an asychronous build only for the production client?

There is! Since webpack is responsible for compiling the production build we could use one of the many available plugins. Let’s use NormalModuleReplacementPlugin that comes with webpack to quickly hack it together:

if ( process.env.NODE_ENV === 'production' ) {
config.plugins.push(
new webpack.NormalModuleReplacementPlugin(
/^\.\.\/routes\/Routes$/,
'../routes/RoutesAsync'
)
);
}
/* | SERVER | CLIENT |
-------------------------------
DEVELOPMENT | SYNC | SYNC |
-------------------------------
PRODUCTION | SYNC | ASYNC |
-------------------------------
*/

That’s it?

You bet. Hot reloading is only a concern during development. Code-splitting is only a concern for production builds. You don’t want users downloading huge monolithic JS files before the app becomes usable. But you want to be able to iterate as fast as possible during coding. It’s a win-win.

Doesn’t this violate the 4th requirement?

Yes and no. Yes, we now have to update two files when we add a route:

  • ./Route.js which is always used
  • ./RouteAsync.js which is only used for client production builds

But this applies only to async routes. It doesn’t mean all routes have to be lazy. Also typing involved is about the same. Since all other code is 100% the same, we can get away with it.

What if I want to test my async loading code?

You can comment out the production check in webpack.config.js. Hot Reload will stop working temporarily, but we can live with that. You won’t have any problems with server-side rendering.

What was that div we wrapped <Match>s in?

This is used only in production builds. We get a full page’s worth of markup on the server, the UI is rendered and CSS applied, only to have the lazy route replacing everything with a loading spinner while the chunk is being downloaded. This is a major problem.

We don’t want the page to disappear while we load the first route. So we have a special case just for that first load.

  1. In our entry point we store that div’s innerHTML in memory. We do that before anything else.
  2. When the first async component renders the first pass, we check if it is the first route that was ever loaded.
  3. If it is the first route, we render a plain div with and dangerouslySetInnerHTML the markup we have stored in memory.

It is an ugly hack, but it works.

How do we avoid waterfalls?

We take advantage of resource prioritization. This involves injecting our entry script and chunk(s) URLs in the head so they start downloading immediately.

<link rel="preload" href="[bundlehash].js" as="script">
<link rel="preload" href="[chunkhash].js" as="script">

Webpack has a JSON output mode which produces detailed machine-readable information about everything that has been included in a generated bundle. It also has information about the code-split chunks.

webpack --json > manifest.json

With an offline Node script we can find all generated file names and update our app’s config file:

const fs = require('fs');
const manifest = require('../manifest.json');
const config = require('../config.json');
const configUpdated = Object.assign({}, config);

configUpdated.manifest = {
js: manifestJs.assetsByChunkName.main,
};

configUpdated.routes = {};

manifestJs.chunks.forEach(chunk => {
if ( chunk.entry === true ) {
return;
}

const route = chunk.modules[0].name.match(
/routes[/]([a-z][a-z-_/]+)[/]index.js$/
);

if ( route !== null ) {
if ( route[1] === 'home' ) {
configUpdated.routes['/'] = chunk.files[0];
} else {
configUpdated.routes[`/${route[1]}`] = chunk.files[0];
}
}
});

fs.writeFileSync(
'./config.json',
JSON.stringify(configUpdated, null, 2)
);

Matching paths with route module names is not ideal but it gets the job done

The difference can be huge. Especially if you are on HTTP/2 that multiplexes in a single TCP connection. You can even do server push if you like.

Before:

Naive lazy loading

After:

Lazy loading with preload hints

What about scrolling?

Haven’t found a solution yet. The previous version used react-router-scroll but it is no longer compatible with React Router v4. Fortunately, for this project it isn’t a deal-breaker.

Does this work with nested async routes?

Not really sure, since we haven’t needed nested routes so far. It should work but avoiding waterfalls can get really tricky.

What is LWJGL anyway?

LWJGL is a Java library that enables cross-platform access to popular native APIs useful in the development of graphics (OpenGL), audio (OpenAL) and parallel computing (OpenCL) applications. This access is direct and high-performance, yet also wrapped in a type-safe and user-friendly layer, appropriate for the Java ecosystem.

It has been used in a number of games, most notably Minecraft.

--

--