-- How do I do this again?
Another year, another React app to add server-side rendering support to. For the uninitiated, Server-Side-Rendering in the context of React is the term used to describe the use of React itself to do the first render pass of a web-app on the server and send that to the browser just before it gets the (rather large) JavaScript bundle for your React App. The browser can display the server-rendered page immediately and then continue running your App once it is done with the bundle.
Server rendering really is one of those things where its great in theory, great when you actually have it working, and terrible when things are half broken and you don't know why.
So of course the purpose of this post is to get you out of that "terrible" zone as soon as you can so that you might be able to get on to building your app as opposed to digging deep into pre-compiled node packages all week trying to work out why things aren't working.
If you don't already have a React App, setting up server rendering on your App's server is really easy (assuming that you use Express, like most people). You just use an ES6 compliant JavaScript engine to run the following.
const express = require('express');
const React = require('react');
const ReactDOM = require('react-dom');
const app = express();
const HelloWorld = ({ text }) => (
<p>{text}</p>
);
app.use("/static", express.static("build"));
app.get('*', (req, res) => {
const appHTML = ReactDOM.renderToString(<HelloWorld text="Hello, World" />);
res.status(200);
res.send(`
<html>
<head>
<title>My awesome server-rendered app!</title>
<script src="/static/main.js" />
</head>
<body>
<div id="app">${appHTML}</div>
</body>
</html>
`)
});
Done, your app has server rendering! Or does it?
In 2018 time has marched forward and everything is different again. But unsurprisingly what hasn't changed is that adding Server Side Rendering is still an exercise in guesswork. Lets say for instance, that you're using the best practices described in a project like react-boilerplate. react-boilerplate doesn't support Server Side Rendering (yet) so it seems like a good place to start. As a starter project it has lots of things that a "best practices" react project might have set up, like webpack, styled-components, redux, redux-saga, react-router and react-loadable. All of those things are really handy! Unfortunately, they all require a little bit of configuration and tweaking to work with server-side rendering. Off we go, I guess.
You would have noticed that in the server above, we're using both JSX and ES6 syntax. That's great and node supports a lot of ES6 stuff natively now. Except for ES6 imports, which you're probably already using heavily. babel-node to the rescue right? Not so fast - babel-node is really meant to be a developer-only tool and holds its compiler cache in memory. It really shouldn't be used for production servers. Also, strictly using babel-node doesn't solve the problem that if you import other react libraries, they might well want to depend on loaders to import things like CSS or images, things that won't work in Node natively.
Instead, we're going to use webpack to build a node-compatible server bundle and then run that bundle from Node. So just change your entrypoint to your server in your webpack config, right?
{
entry: ['server/index.js']
}
Not quite:
...
ERROR in ./node_modules/fsevents/node_modules/node-pre-gyp/lib/util/compile.js
Module not found: Error: Can't resolve 'child_process' in 'web/react-boilerplate-serverless/node_modules/fsevents/node_modules/node-pre-gyp/lib/util'
@ ./node_modules/fsevents/node_modules/node-pre-gyp/lib/util/compile.js 9:9-33
@ ./node_modules/fsevents/node_modules/node-pre-gyp/lib ^\.\/.*$
@ ./node_modules/fsevents/node_modules/node-pre-gyp/lib/node-pre-gyp.js
@ ./node_modules/fsevents/fsevents.js
@ ./node_modules/chokidar/lib/fsevents-handler.js
@ ./node_modules/chokidar/index.js
@ ./node_modules/watchpack/lib/DirectoryWatcher.js
@ ./node_modules/watchpack/lib/watcherManager.js
@ ./node_modules/watchpack/lib/watchpack.js
@ (webpack)/lib/node/NodeWatchFileSystem.js
@ (webpack)/lib/node/NodeEnvironmentPlugin.js
@ (webpack)/lib/webpack.js
@ ./server/middlewares/addDevMiddlewares.js
@ ./server/middlewares/frontendMiddleware.js
@ ./server/server.js
@ ./server/index.js
@ multi ./server/index.js
The first problem is that your server probably imports express, which probably requires a lot of things that are either internal node packages or are binary modules. Webpack doesn't know how to handle these so it errors out. Instead, we're going to have to tell webpack to stop doing its job and at least for the server builds, leave all the stuff in node_modules
alone. The result is that if you looked at the corresponding webpack bundle, instead of all the modules being inlined, they just wrap the existing require
statements that were already in use:
/***/ "chalk":
/***/ (function(module, exports) {
eval("module.exports = require(\"chalk\");n");
/***/ }),
This means a couple of things. First of all, we're going to have to decouple our server and client configurations. And we probably want different configurations for development and production builds too. Then, you'll need to tell webpack to exclude all the node_modules
requirements from bundling, which is easily done with webpack-node-externals
. You'll also want target: 'node'
in your webpack configuration file.
entry: [
path.join(process.cwd(), 'server/index.js'),
],
externals: [nodeExternals()],
output: {
filename: 'server.js',
path: path.join(process.cwd(), 'build'),
},
target: 'node',
server: true,
NPM Tasks: Then, you'll want to set up some NPM tasks to build the server bundle:
"build:dev:server": "cross-env NODE_ENV=development webpack --config internals/webpack/webpack.dev.server.babel.js --color --progress",
"build:server": "cross-env NODE_ENV=production webpack --config internals/webpack/webpack.prod.server.babel.js --color -p --progress --hide-modules --display-optimization-bailout",
You'll notice different options for the developmenta nd production builds. In short, you'll want to use -p
for production, since that automatically turns on things like uglifyjs, optimisation, code splitting, etc.
Webpack DLL: Next, if you're building a webpack DLL, like react-boilerplate does, you'll want to exclude any server-only dependencies from it, since that DLL is really meant to be for the client.
Server DefinePlugin: Unfortunately, there are still modules out there which assume the
presence of the browser as soon as they get imported or their components mounted. For those
cases, you're going to have to adapt your React code based on whether or not it is being
compiled for the Server for for the Client. Best way to do this is with a DefinePlugin
. I
had one in my base configuration that was turned on depending on whether or not we were building for the server:
plugins: options.plugins.concat([new webpack.DefinePlugin({
// Put a define in place if we're server-side rendering
...(options.server ? {
__SERVER__: true,
} : {
__SERVER__: false,
}),
})])
In your code, you can use the __SERVER__
variable to conditionally do things depending
on whether code is running on the client or the server.
There's a couple of things you just can't do in your bundle if you want to run on the server.
Don't try and import JSON that you intend to read at runtime: Node lets you do this
with require()
, but since webpack handles require()
at build-time, this will both
blow up since you don't have the relevant loader configured, but also won't load
the JSON at runtime if it only gets generated at runtime. Instead,
change
usage of require()
to fs.readFileSync
or similar.
Prevent modules from trying to load images or CSS directly: In the worst case, you
might need to configure null-loader
to force modules that do the wrong thing to stop doing that. In better cases, you can
define
environment variables
to to tell server-friendly modules to do the right thing.
Wrap modules that do bad things: In some cases, you might end up importing modules
that immediately try and access browser properties on require
. This is particularly
nefarious, though it can be dealt with.
Wrap
the offending module in another component which defines the relevant property to something sensible and then unsets it. You will probably also want to remove the offending module
from the webpack DLL and
exclude
it from webpack-node-externals
too, since external requires on the server side are
immediately evaluated on load, giving you no opportunity to monkey patch the relevant
properties.
So hopefully at this point you have something that server-side-renders, except you get the dreaded flash-of-unstyled-content (aka FOUC) before client side rendering takes over.
Turns out that if you're using styled-components, you have a little bit of extra work to do.
By default styled-components uses some magic to inject <style>
tags into the
<head>
of the DOM, except that doesn't work if you don't have a DOM when you're rendering.
Luckily, styled-components has a little
helper to collect
up all the <style>
tags so that you can inject them into the <head>
of your
server-rendered page yourself.
const stylesheet = new ServerStyleSheet();
const html = ReactDOMServer.renderToString(
stylesheet.collectStyles(
<Root />
)
);
const styleTags = stylesheet.getStyleTags();
res.status(200);
res.send(`
<html>
<head>
<title>My awesome server-rendered app!</title>
${styleTags}
<script src="/static/main.js" />
</head>
<body>
<div id="app">${appHTML}</div>
</body>
</html>
`);
Dependencies styled-components version pinning: Unfortunately, life isn't that simple.
Server-Side-Rendering support was only introduced in styled-components 2.0.0. If one of
your dependencies has a pinned dependency on an older version, then they won't be collected
as part of the style tags, since the version of styled-components it uses won't know
to insert those style tags into the intermediate component that collectStyles
creates.
Thankfully, npm
doesn't make this too hard. With the resolutions
attribute you can force
all installations of a dependency to be a particular version:
"resolutions": {
"styled-components": "^2.4.0"
}
If you're using react-router
in your app to connect different pages to different routes
in the URL bar then there's slightly different configurations you'll need to apply in
the server-side case. If you only support client side rendering, you probably have a
browserHistory
object connected to your redux store and you have ConnectedRouter
using that history. Since that depends on browser-only properties, that obviously won't
work on the server side.
Instead, you'll need to create a memoryHistory
object from the current request URL
and inject both your redux store and and history object into your App's <Root>
component
and use those instead of the browserHistory
on the client side.
import createMemoryHistory = from 'history/createMemoryHistory');
import { routerMiddleware } from 'react-router-redux';
import { createStore, applyMiddleware, compose } from 'redux';
...
function configureStore(initialState = {}, history) {
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [
routerMiddleware(history),
];
const enhancers = [
applyMiddleware(...middlewares),
];
const store = createStore(
createReducer(),
initialState,
compose(...enhancers)
);
return store;
}
...
const memoryHistory = createMemoryHistory(req.url);
memoryHistory.push(req.originalUrl);
const store = configureStore({}, memoryHistory);
const html = ReactDOMServer.renderToString(
<Root history={memoryHistory} store={store} />
);
Using react-loadable on the client side is a great way to speed up page loads by asynchronously loading expensive parts of your app once the 'shell' is loaded. On the server side that's not so useful since all that happens is that a pointless asycnhronous request gets fired on the server side which and by the time it resolves you've already rendered and it is too late.
Instead, what you can do is to collect up the loadables into separate script tags and ship them to the client on the server-side render. Unfortunately, this is not quite as simple as it looks - there's a few things that need to be done here in order to make this work.
Babel Plugin: First, add the react-loadable/babel plugin to your babel plugins:
"plugins": [
"react-loadable/babel"
],
Webpack Plugin: Then,
use
the ReactLoadablePlugin
on your client-side webpack build config to generate
a manifest of webpack chunks corresponding to each module. We'll read this file on
the server side to inject script tags for your loadables.
ModulesConcatenationPlugin: Unfortunately, as of 1 March 2018, react-loadable hasn't shipped a release that fixes compatibility with this plugin, so you may need to disable it.
Split out manifest bootstrap: Since we'll be preloading the chunks before your main
manifest, we'll need to preload the webpack manifest bootstrap code before preloading
those chunks! That's easily done by using CommonChunksPlugin
in your client webpack
config (note that ).
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
filename: 'manifest.js',
minChunks: Infinity,
})
HTMLWebpackPlugin:: If you're using HTMLWebpackPlugin
to build your HTML file
you'll need to prevent HtmlWebpackPlugin
from including the manifest chunk,
since we'll manually include it in the right place later.
new HtmlWebpackPlugin({
inject: true, // Inject all files that are generated by webpack, e.g. bundle.js
template: 'app/index.html',
excludeChunks: ['manifest'],
})
Server Side Renderer: Now you can read the manifest into your server-side renderer
and capture the loadables that would have been requested on the given route and preload
them into <script>
tags on the rendered page.
const modules = [];
const html = ReactDOMServer.renderToString(
<Loadable.Capture report={(moduleName) => modules.push(moduleName)}>
<Root />
</Loadable.Capture>
);
fs.readFile('./build/react-loadable.json', 'utf-8', (statsErr, statsData) => {
const bundles = getBundles(JSON.parse(statsData), modules);
const bundlesHTML = bundles.map((bundle) =>
`<script src="/static/${bundle.file}"></script>`
).join('\n');
res.status(200);
res.send(`
<html>
<head>
<title>My awesome server-rendered app!</title>
<script src="/static/manifest.js">
${bundlesHTML}
<script src="/static/main.js" />
</head>
<body>
<div id="app">${appHTML}</div>
</body>
</html>
`);
});
Client side preloadReady: You'll also want to prevent the client side from doing any rendering until all the bundles are preloaded:
Loadable.preloadReady().then(() => {
ReactDOM.render(
<Root />,
MOUNT_NODE
);
});
Usually when your components mount there's a bunch of data you might want to immediately start fetching from the server. If it is cheap to do so, it might be beneficial for you to do some of that work on the server to avoid a roundtrip. To do that, you'll want to wait for your redux sagas to complete until rendering the final result.
The gist of what will happen here is that we'll kick off a render of your application, run
any sagas which need to be run, dispatch a special END
event, which causes the
saga generators to terminate, then render your application again, this time with
an updated redux store containing pre-filled data.
import { END } from 'redux-saga';
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
export default function configureStore(initialState = {}) {
const middlewares = [
sagaMiddleware
];
const enhancers = [
applyMiddleware(...middlewares),
];
const store = createStore(
initialState,
compose(...enhancers)
);
// Extensions
store.runSaga = sagaMiddleware.run;
store.injectedSagas = {}; // Saga registry
return store;
}
const store = configureStore({});
ReactDOM.renderToString(<Root store={store} />);
store.dispatch(END);
store.runSagas(sagas).then(() => {
const html = ReactDOM.renderToString(<Root store={store}>)
});
So now the content of your page has rendered, except that on the server side, you built up some redux state which the client side knows nothing about! That means that when the client side re-renders it'll do so without the benefit of that state and probably means that React won't be able to re-use a lot of your server-rendered markup.
What you'll need to do is serialize the redux state and send that over to the client such that the client can "rehydrate" from that state and continue where the server left off.
Thankfully, that's pretty straightforward. Just encode the server state as JSON
and assign it to variable in a <script>
tag that gets executed on the server side:
const stateHydrationHTML = (
`<script>window.__SERVER_STATE = ${JSON.stringify(store.getState())}</script>`
);
res.status(200);
res.send(`
<html>
<head>
<title>My awesome server-rendered app!</title>
${stateHydrationHTML}
<script src="/static/main.js" />
</head>
<body>
<div id="app">${appHTML}</div>
</body>
</html>
`);
Client side: On the client side, you'll want to read the __SERVER_STATE
property
if it exists and then initialize the store from there:
const initialState = window.__SERVER_STATE || {};
const store = configureStore(initialState);
If you're planning on doing a serverless server-side-rendered app you'll need to create a webpack bundle for the serverless deployment too. Thankfully, that's just a matter of making a "webpack library" out of your existing serverless entry point.
{
entry: [
path.join(process.cwd(), 'lambda.js'),
],
externals: [nodeExternals()],
output: {
filename: 'prodLambda.js',
libraryTarget: 'umd',
library: 'lambda',
// Needs to go in process.cwd in order to be imported
// correctly from lambda
path: path.join(process.cwd()),
},
target: 'node',
}
Obviously, its a lot easier to start from a worked example of adding server-side rendering to an app and use the blog post for context. So I've done just that in my react-boilerplate-serverless fork. Feel free to check it out and fork it.
Thanks to Jack Scott for reviewing a draft of this post.