💪 React + Router + Redux and Server Side Rendering2017-02-28
At my work I use React as the primary UI framework, adoption started with the service site, a huge web application (hundreds of screens and dozens of flows and wizards) that allows users and admins to configure the system. Successful technologies and approaches tend to eventually propagate more and more to other products of the company, so the React expansion was not a surprise and now React stack is used as a standard approach.
Unfortunately, there is no silver bullet, requirements differ between projects, so at some point it became obvious that we no longer can live with just simple client side rendering and should take a look on a server side rendering.
This is one of the articles about the research that has been done in scope of React, Redux, React Router and other React-* technology stack in conjunction with Server Side Rendering approach.
In this article we will build our own, flexible solution based on best practices of Webpack, React Router and React Redux. If you want something less flexible but more simple, take a look at Next.js with Redux. Also if you are interested you may take a look on a full research and comparison of React-based solutions.
Server Side Rendering sequence in a nutshell
There is a whole new class of challenges in client-side single page apps, but the main issue always was the start up time. Users have to wait while the initial HTML loads, then CSS and JS, then app bootstraps, then it renders, then data is fetched, then it renders again with data. Only at this point the app becomes usable. Server side rendering allows to deliver more stuff up-front. This becomes crucial if we make flows that are embedded into native mobile apps, where we want seamless experience between native and web.
Number one objective is to reach the certain page, obey all the rules defined in React Router’s routes, then dispatch some actions, then render the page and send it to client. Fairly straightforward.
The Client
Client requires some preparations in order to be correctly server-rendered.
- I highly recommend to add so-called code split points so that each page will be a separate Webpack chunk. This will allow to download only necessary stuff when client is initializing.
- Get rid of anything related to window object or global scope (all your location, history, document, direct DOM manipulation, etc.). There is no such stuff on a server and usage of global is a very bad practice by definition. You will surely get an overlap between requests your code has shared state somewhere except the Redux Store. Global can be used very carefully and only if you really understand why you need it.
- Make sure your client code does not leak. Every memory leak will become a problem when it’s no longer a client browser with tons of memory, but even there it’s a big no-no. On a server with very limited resources and potentially high load it will become crucial.
- Check your 3rd party libraries, they can be unaware of the terms “universal” or “isomorphic”.
Router
This is probably the simplest part, we only need to create a function that will return routes.
import Reactfrom "react";
import {IndexRoute, Route}from "react-router";
import NotFoundfrom './NotFound';// this is needed to extract default export of async route
function def(promise) {
return promise.then(cmp => cmp.default);
}exportdefaultfunction() { return <Route path='/'>
<IndexRoute getComponent={() => def(import('./App'))}/>
<Route path='*' component={NotFound}/>
</Route>;}
Redux Store
Some folks export an instance of Redux Store, so it acts like an app-wide singleton. That’s not the case for server rendering where each request is unique and should have personal Redux Store. So we should follow best practices and export a createStore
function instead:
import{createStore}from "redux";
importreducersfrom "./reducers";
export default functionconfigureStore(initialState) {
returncreateStore(
reducers,
initialState
);
}
Initial state must be an argument because we will use it later.
Endpoint page
Router allows the server or client to reach the main content page. Usually it has the list of things or a certain content like a blog post or an article. We’d like to dispatch some actions before we ship the resulting HTML to the client, for example, load that list or article. We can do it in a special static method of the endpoint Component. We will specifically follow the Next.js-alike naming: getInitialProps
. In this method we can prepare our Redux Store’s state
so that when component will be rendered, the state
will have all the right things, so the resulting HTML will have them too.
The page should look like this:
import {withWrapper}from "create-react-server/wrapper";
@connect(state => ({foo: state.foo}))
@withWrapper
export default class Pageextends Component {
async getInitialProps({store}) {
await store.dispatch({type: 'FOO', payload: 'foo'});
}
render() {
return (
<div>
<div>{this.props.foo}</div>
</div>
)
}
}
The leaf node for 404 pages should also have some static property to determine that it’s an error: notFound
should work:
exportdefaultclass 404Pageextends Component {
static notFound = true;
render() {
return (
<h1>Not Found</h1>
)
}
}
Application Init
Now we need to render the app. The hackery is especially required if you use async routes, those have to be parsed first and only after that they can be rendered, otherwise you’ll get a blank page.
Here is the example of the main app endpoint:
import Reactfrom "react";
import {render}from "react-dom";
import {Provider}from "react-redux";
import {browserHistory as history, match, Router}from "react-router";
import {WrapperProvider}from "create-react-server/wrapper";import createRoutesfrom "./routes";
import createStorefrom "./reduxStore";const mountNode = document.getElementById('app');
const store = createStore(window.__INITIAL__STATE__);function renderRouter(routes, store, mountNode) { match({history, routes}, (error, redirect, props) => { render((
<Provider store={store}>
<WrapperProvider initialProps={window.__INITIAL__PROPS__}>
<Router {...props}>
{createRoutes()}
</Router>
</WrapperProvider>
</Provider>
), mountNode); });}renderRouter(createRouter(browserHistory), store, mountNode);
Bedrock HTML
Here is the sample index.html
that should be a part of webpack build, e.g. emitted to you output path. It could be a real file or generated by HtmlWebpackPlugin
.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>App</title>
<body>
<div id="root"></div>
</body>
</html>
The Server
Server has to do pretty much the same thing as client side, but with one major difference: it should have a unique Redux Store for each request, unlike the client, where the Store is a singleton.
When request comes to the server:
- it performs the same matching logic of Routes to determine the endpoint page, if nothing matches, it tries to serve as static
- creates the fresh Redux
Store
- injects
Store
ingetInitialProps
- waits until all async actions will be dispatched
- serializes and
Store
’sstate
injects it into template HTML - renders React app to an HTML string
- sends everything to client
Step 5 is absolutely crucial because otherwise the client side will not be applied correctly on initial HTML and you will get a nasty warning that you’re loosing all benefits of server rendering since the app has just been rendered from scratch in order to avoid inconsistency.
Preparations
We assume that you should already have express
, webpack
and webpack-dev-server
. You will need them.
Server rendering basically takes your application, applies same Babel transformations just like Webpack does, then renders it to an HTML string. Because of that you will also need a babel-cli
since it's the easiest way to run the server with Babel:
npm installbabel-cli express webpack webpack-dev-server html-webpack-plugin --save-dev
We assume that you either have .babelrc
file or babel
section in your package.json
so that Webpack’s Babel and NodeJS Babel have same config. Keep in mind that if you use babel-plugin-syntax-dynamic-import
, then you can only use it in Webpack config, Babel config of Node should not have babel-plugin-syntax-dynamic-import
, instead you should use babel-plugin-dynamic-import-webpack
and babel-plugin-transform-ensure-ignore
(first import()
replaces require.ensure
, second changes require.ensure
to regular synchronous require
).
Alter your package.json
and add to scripts
section:
{
"scripts": {
"build": "webpack --progress",
"start": "webpack-dev-server --progress",
"dev-server": "NODE_ENV=development babel-node ./index.js",
"server": "NODE_ENV=production babel-node ./index.js"
}
}
For convenience webpack.config.js
should have devServer
section, where port and content base should be specified, also we should add HtmlWebpackPlugin to plugins
section:
var HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
//...
output: {
path: process.cwd() + '/build',
publicPath: '/',
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
favicon: './src/favicon.ico', // optional
template: './src/index.html',
}),
],
devServer: {
port: process.env.PORT || 3000,
contentBase: './src',
},
//...
};
React Router Redux Server Rendering Middleware
In order to make the above-mentioned sequence to work we can implement it from scratch or use the create-react-server package, that has been created specifically for this purpose.
npm install create-react-server --save-dev
We would like to use benefits that webpack-dev-server
provides, so our server side will have two modes: development and production. At the same time we will use same rendering mechanism for both modes: real file system for production and memory file system for development. This is also a responsibility of middleware.
Static Server
Now let’s set up the server.js
, you can add more customizations if needed.
import path from 'path';
import Express from 'express';
import webpack from 'webpack';
import Server from 'webpack-dev-server';
import config from './webpack.config';
const port = process.env.PORT || 3000;
if (process.env.NODE_ENV !== 'production') {
const compiler = webpack(config);
new Server(compiler, config.devServer).listen(port, '0.0.0.0', listen);
} else {
const app = Express();
app.use(Express.static(config.output.path));
app.listen(port, listen);
}
function listen(err) {
if (err) throw err;
console.log('Listening %s', port);
}
Server Side Renderer middleware
Once we have the basic static server we can import and configure the middleware:
import createRoutes from './src/routes';
import createStore from './src/store';
import { createExpressMiddleware, createWebpackMiddleware, skipRequireExtensions } from 'create-react-server'; // may be omitted but then tell NodeJS to not require non-js files
skipRequireExtensions();
const options = {
createRoutes: () => createRoutes(),
createStore: ({ req, res }) =>
createStore({
foo: Date.now(), // pre-populate something right here
}),
templatePath: path.join(config.output.path, 'index.html'),
outputPath: config.output.path,
};
Please note that template({template, error, html, store, initialProps, component, req, res})
function also can do more sophisticated things like finding <h1>
tags (or taking it from initialProps
or component
) and replacing title
tag with it’s content, this is good for SEO. Also instead of simple replace
you may use any template engine you like. The resulting return should be a plain string.
You can also provide errorTemplate
for server-side errors when client app was not even rendered.
Next we rewrite the sections where we created servers:
if (process.env.NODE_ENV !== 'production') {
const compiler = webpack(config); // we are adding this
config.devServer.setup = function (app) {
app.use(createWebpackMiddleware(compiler, config)(options));
};
new Server(compiler, config.devServer).listen(port, '0.0.0.0', listen);
} else {
const app = Express(); // we are adding this, order is important
app.use(createExpressMiddleware(options));
app.use(Express.static(config.output.path));
app.listen(port, listen);
}
Full example is available at: https://github.com/kirill-konshin/create-react-server/tree/master/examples/webpack-blocks.
Room for improvements
Recursively check the component tree looking for getInitialProps
The goal is to have completely rendered website, not just content area. There is an existing issue with synchronization of all such calls, the order has to be always predictable.
Pre-built server-side scripts
First of all, we can have a second Webpack config (or a multi-config, when we return an array of configs instead of one config object), that will have target: "node", library: "commonjs"
and build it along with client side. In this case we won’t have to bother about non-js extensions, Babel in runtime, and so on, everything will be pre-built and then executed in a plain mode.
The entry point has to only export createRoutes
and createStore
functions, the rest will come along.
Optimization of renderToString
Eventually it may appear, that the bottleneck of the server performance is renderToString
method of React DOM. In order to fight with it we can take something like https://github.com/walmartlabs/react-ssr-optimization.