After several days of development, Folleon finally supports server-side rendering (SSR). This decreases the initial load time for new users and improves SEO. From a previous average 1s (https://folleon.com/21), Folleon loads now initially taking only 500ms. This is great for new users and search engines.

As the web-application is built using React and [create-react-app (CRA) (https://github.com/facebook/create-react-app) we wanted to implement server-side rendering on top of that. An alternative would be to eject the app and go from there on our own. But if this is not necessary, we don't want to do it.

There are many problems one may run into when enabling SSR not directly at the beginning of a project. Surprisingly, the final solution is quite simple, so we decided to share it here.

Let's go!

Adapting the existing React app

First we need to do some refactoring to prepare the existing app: the app's root component needs to be usable from two points, the existing React application in the browser, and the server for rendering there. In the src/index.js we need to extract all the routes to get to something like this:

import { BrowserRouter, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import React from 'react';
import Reducers from './helpers/reducers';
import Routes from './routes';
import { ScrollContext } from 'react-router-scroll-4';
import { createStore } from 'redux';
import { hydrate } from 'react-dom';

const store = createStore(Reducers);

hydrate(
  <Provider store={store}>
    <BrowserRouter>
      <ScrollContext>
        {/* The routes must be mapped exactly like here (to avoid issues with history) */}
        <Route component={Routes} />
      </ScrollContext>
    </BrowserRouter>
  </Provider>,
  global.window.document.getElementById('root')
);

Also note that we use hydrate instead of render to allow React to reuse the already rendered HTML on the browser.

The src/routes.js file looks something like this (nothing special here):

import React, { Component, Fragment } from 'react';
import ContactScene from './scenes/contact/contact.scene';
import Header from './components/header/header';
import HomeScene from './scenes/home/home.scene';
import { Route } from 'react-router-dom';
import { Switch } from 'react-router-dom';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';

class Routes extends Component {
  render = () => (
    <Fragment>
      <Header />
      <Switch>
        <Route path="/contact" component={ContactScene} />
        ...
        <Route path="/" component={HomeScene} />
      </Switch>
    </Fragment>
  );
}

export default connect(state => ({ state }))(withRouter(Routes));

This part can now easily be reused for the browser and the server-side rendering. All client-only parts such as ScrollContext or the custom Redux store are added by index.js only.

Using Express for rendering

For the actual rendering we use Express.

src/server/bootstrap.js for making the node environment ready reading our React/JSX/ES6 code:

// Ignore styles
require('ignore-styles');

// Use Babel with env and react-app
require('babel-register')({
  ignore: /\/(build|node_modules)\//,
  presets: ['env', 'react-app']
});

// Mock a window object, also needed for dependencies
global.window = new (require('window'))();
global.document = global.window.document;
global.navigator = global.window.navigator;

// Load the server
require('./server');

src/server/server.js where we start the express server:

import bodyParser from 'body-parser';
import express from 'express';
import path from 'path';
import renderer from './renderer';

const app = express();
const PORT = process.env.PORT || 8000;

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.get(/^\/((index|contact|...).*|[0-9]+)?/, renderer); // Here we pass all requests that should be rendered
app.get('/*', express.static(path.join(__dirname, '../../build'))); // Everything else goes to the static files

app.listen(PORT, () => console.log(`App listening on port ${PORT}!`));
app.on('error', err => console.error(`Error`, err)); // You need to add proper error handling and logging

src/server/renderer.js for building the rendered HTML, loading the data asynchronously, and returning all together to the client:

import Helmet from 'react-helmet';
import { INITIAL_STATE } from '../helpers/reducers';
import { Provider } from 'react-redux';
import React from 'react';
import Routes from '../routes';
import { StaticRouter } from 'react-router-dom';
import { createStore } from 'redux';
import fs from 'fs';
import network from '../helpers/network';
import path from 'path';
import reducers from '../helpers/reducers';
import { renderToString } from 'react-dom/server';

Helmet.canUseDOM = false; // Helmet is used in our React app, thus we need it here, too

// Load the production-built index.html once
const untouchedHtml = fs.readFileSync(path.resolve(__dirname, '../../build/index.html'), 'utf8').toString();

export default (req, res) => {
  // We create a custom initial store, for each request
  const store = createStore(reducers, INITIAL_STATE);

  const body = (
    <Provider store={store}>
      <StaticRouter location={req.url}>
        <Routes />
      </StaticRouter>
    </Provider>
  );

  // Build the content a first time, collecting promises to resolve.
  let rendering = renderToString(body);
  const promises = store.getState().serverSideRenderBlockingPromises;

  return Promise.all(promises)
    .then(() => {
      // Re-render if promises were resolved - this means that data was loaded and added
      if (promises.length > 0) rendering = renderToString(body);

      // Finalize and send
      const title = store.getState().title;
      let touchedHtml = untouchedHtml.replace(
        '<title></title>',
        `<title>${title ? `${title} – Folleon` : 'Folleon'}</title>`
      );
      touchedHtml = touchedHtml.replace('<div id="root"></div>', `<div id="root">${rendering}</div>`);
      Helmet.renderStatic();
      res.send(touchedHtml);
    })
    .catch(err => {
      console.log('Error (send untouched index.html):', err);
      res.send(untouchedHtml);
    });
};

bin/renderer to start the application (you may want to use a node process manager like PM2 (https://pm2.keymetrics.io/) to easily set up a node/express cluster):

#!/usr/bin/env node

/**
 * This application serves server-side rendered HTML documents to bootstrap
 * the client's page.
 *
 * This application is supposed to run at all times, and in a cluster
 */

process.env.NODE_ENV = process.env.NODE_ENV || 'production';
require('../src/server/bootstrap');
Asynchronously loading with ReactDomServer.renderToString

React's renderToString takes a component and returns the rendered HTML string. This function is synchronous, hence doesn't resolve any promises and doesn't load data.

As shown in renderer.js, we invoke renderToString twice if there are any promises to resolve. This is obviously a poor solution in terms of performance on high load, but it works as a start. To address this issue we'll probably implement something like the solution provided by Anver Sorek (https://hackernoon.com/asynchronous-server-side-rendering-with-react-3860c05f8a5e). However, this is not necessary just yet.

A page (we call them Scenes) just needs to load and dispatch all data gathering promises in the componentWillMount method. Compared to componentDidMount, it is also invoked during server-side rendering.

src/scenes/home/home.scene.js does the loading part like this:

import { withRouter } from 'react-router-dom';
import React, { Component } from 'react';
import { pushServerSideRenderBlockingPromise, storeValue } from '../../helpers/actions';
import { connect } from 'react-redux';
import network from '../../helpers/network';

class HomeScene extends Component {
  state = {};

  componentWillMount = () => {
    if (!this.props.state.posts) this.props.dispatch(pushServerSideRenderBlockingPromise(this.loadPosts()));
  };

  loadPosts = () =>
    (this.props.state.authenticated
      ? network.getPostsUserFeed(this.props.state.id)
      : network.getPostsGlobalFeed()
    ).then(res => this.props.dispatch(storeValue('posts', res.data)), err => network.handleError(err, this.props));

  render = () => (
    <div className="scene home">
      <div className="content">
        ...
      </div>
    </div>
  );
}

export default connect(state => ({ state }))(withRouter(HomeScene));

The method with the unhandy name – pushServerSideRenderBlockingPromise – just pushes every given promise to an array. The renderer uses Promises.all to resolve all promises and then render again.

Note that the loaded data is dispatched to the global Redux store. If this store is already filled, the data doesn't need to be loaded and can just be rendered. That's what happens in the second rendering run.

Running and testing the application

Run the application locally with npm run build && node ./bin/renderer as it first needs to be built for production, and then rendered upon request.

Conclusion

Conceptually we render the page a first time, collect all promises that load data, resolve them, and render again. The promises needed to do so are stored in the request's global Redux store. Once all is done, we render the HTML again and send the HTTP response to the user.

This solution works with using create-react-app, and without ejecting.

Thoughts, ideas, questions? Write a comment.

Thanks to Patrick Carsen (https://medium.com/@cereallarceny/server-side-rendering-with-create-react-app-fiber-react-router-v4-helmet-redux-and-thunk-275cb25ca972) and Anver Sorek (https://hackernoon.com/asynchronous-server-side-rendering-with-react-3860c05f8a5e) for their posts on this topic.