React Redux Starter Kit – enable server side rendering

During the last days I tried to make an application with server side rendering – only with NodeJS. Therefore I selected React due to its big performance and flexibility. The next question was to decide which Flux library I should use and take a look at Redux, as well as Alt.js. Redux fascinates more than Alt.js because it provides big flexibility and it is much smaller than Alt.js (but unfortunately more complicated to learn and use).

As a base I selected the React Redux Starter Kit, because it seems to be good maintained and structured. The starter kit works great, but all it needs was server side rendering. Before I continue I want to refer to another starter kit which still implements server side rendering (and which works also great). If you simple search for a method to become server side rending working then choose this starter kit, but if you want to understand how the server side rendering is working, then is this tutorial perfect for you.

For getting started download the React Redux Starter Kit from Github:

$ git clone https://github.com/davezuko/react-redux-starter-kit.git
$ cd react-redux-starter-kit
$ npm install                   # Install Node modules listed in ./package.json (may take a while the first time)
$ npm start                     # Compile and launch

Now we can start with our implementation of our server-side-rendering:

First install some dependencies:

npm install --save express humps webpack-isomorphic-tools

Next create the initializing script for creating the routes and store: src/init.js

import makeRoutes from './routes'
import configureStore from './redux/configureStore'


export default function(initialState, history) {
  const store = configureStore(initialState, history)

  // Now that we have the Redux store, we can create our routes. We provide
  // the store to the route definitions so that routes have access to it for
  // hooks such as `onEnter`.
  const routes = makeRoutes(store);

  return {
    store,
    routes
  }
}

As a first step we make a script which initializes the redux store and create the routes (which are selected from src/routes/index.js).

The next step is to create a server file which starts our Express-Server. Therefore we create a server.js file:

import path from 'path';
import Express from 'express';
import React from 'react';
import fs from 'fs';
import _debug from 'debug'
import { renderToString } from 'react-dom/server';
import { createStore } from 'redux';
import createMemoryHistory from 'react-router/lib/createMemoryHistory';
import { RouterContext, match } from 'react-router';
import { Provider } from 'react-redux';
import Promise from 'bluebird';
import createLocation from 'history/lib/createLocation';

import Root from './containers/Root';
import config from '../config/index';
import init from './init';

const debug = _debug('app:bin:production-server');
const app = Express();

// This is fired every time the server side receives a request
app.use(Express.static('dist'));
app.use(handleRender);

// We are going to fill these out in the sections to follow
function handleRender(req, res) {
  const history = createMemoryHistory();
  const {
    store,
    routes
    } = init({}, true);

  let location = createLocation(req.url);

  match({ routes, location }, (error, redirectLocation, renderProps) => {
    if (redirectLocation) {
      res.redirect(301, redirectLocation.pathname + redirectLocation.search)
    } else if (error) {
      res.send(500, error.message)
    } else if (renderProps == null) {
      res.send(404, 'Not found')
    } else {

      // first resolve page promise (static fetchData())
      getReduxPromise().then(()=> {

        // Render the component to a string
        const html = renderToString(
          <Provider store={store}>
            { <RouterContext {...renderProps}/> }
          </Provider>
        );

        // Grab the initial state from our Redux store
        const initialState = store.getState();

        // get index file
        fs.readFile('./dist/index.html', "utf-8", function read(err, data) {
          if (err) {
            res.send('Could not open index file, please try again later ', 500);
            return;
          }
          const renderedHtml = data.replace('<!-- INITIAL_STATE -->', `<script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}</script>`)
            .replace('<!-- HTML_CONTENT -->', html);
          res.send(renderedHtml);
        });
      });
    }

    function getReduxPromise () {
      let { query, params } = renderProps;
      let comp = renderProps.components[renderProps.components.length - 1].WrappedComponent;
      
      let promise = comp.fetchData ?
        comp.fetchData({ query, params, store, history }) :
        Promise.resolve();

      return promise;
    }
  });
}

const port = config.server_port;
const host = config.server_host;

app.listen(port);
debug(`Server is now running at http://${host}:${port}.`);

How does this script works? I will explain it step by step:

The script first starts the Express server. We use the handleRender function as a Middleware for the server to render our react page component on server side.

const app = Express();

// This is fired every time the server side receives a request
app.use(Express.static('dist'));
app.use(handleRender);

// We are going to fill these out in the sections to follow
function handleRender(req, res) {
  // ...
}

Next we take a closer look at the handleRender function:

const history = createMemoryHistory();

let location = createLocation(req.url);

This part creates a history which is needed later for fetching data asynchronously.

const {
  store,
  routes
  } = init({}, true); // create store and routes with empty initial state

let location = createLocation(req.url);

The next part creates a store in our still created init function (see init.js). It creates the store and routes for us. But we need also a location with our current URL which is stored in the variable location.

Now we can start our route matching process:

match({ routes, location }, (error, redirectLocation, renderProps) => {
    if (redirectLocation) {
      res.redirect(301, redirectLocation.pathname + redirectLocation.search)
    } else if (error) {
      res.send(500, error.message)
    } else if (renderProps == null) {
      res.send(404, 'Not found')
    } else {
      // page is available in our routes
    }
    // ...
});

Errors and redirections are handled right now, but we have to handle rendering our components. Therefore we render our components to a string which is appended to the index.html file. Important is that we append the initial state to the end of the body tag. The server locates the part for adding the html content or the initial state in the HTML based on the comments HTML_CONTENT and INITIAL_STATE.

// Render the component to a string
        const html = renderToString(
          <Provider store={store}>
            { <RouterContext {...renderProps}/> }
          </Provider>
        );

        // Grab the initial state from our Redux store
        const initialState = store.getState();

        // get index file
        fs.readFile('./dist/index.html', "utf-8", function read(err, data) {
          if (err) {
            res.send('Could not open index file, please try again later ', 500);
            return;
          }
          const renderedHtml = data.replace('<!-- INITIAL_STATE -->', `<script>window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}</script>`)
            .replace('<!-- HTML_CONTENT -->', html);
          res.send(renderedHtml);
        });

 

Unfortunately the index.html file does not support those comments yet. Therefore you have to add them to the index file (src/index.html):

<!doctype html>
<html lang="en">
<head>
  <title>React Redux Starter Kit</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
</head>
<body>
  <div id="root" style="height: 100%"><!-- HTML_CONTENT --></div>
  <!-- INITIAL_STATE -->
</body>
</html>

The last part of the server.js script getReduxPromise() function. This function picks the page component and try to execute (if available) the fetchData static function to fetch the data first before rendering the result.

function getReduxPromise () {
      let { query, params } = renderProps;
      let comp = renderProps.components[renderProps.components.length - 1].WrappedComponent;

      let promise = comp.fetchData ?
        comp.fetchData({ query, params, store, history }) :
        Promise.resolve();

      return promise;
    }

 

That`s it!
To execute our server script, we have to create a executable file “/bin/production-server.js” to start our server:

require('../server.babel'); // babel registration (runtime transpilation for node)
import config from '../config';
var path = require('path');
var rootDir = path.resolve(__dirname, '..');

/**
 * Define isomorphic constants.
 */
global.__CLIENT__ = false;
global.__SERVER__ = true;
global.__BASENAME__ = config.env.__BASENAME__;
global.__DEBUG__ = false;
global.__DEVELOPMENT__ = config.env !== 'production';

// https://github.com/halt-hammerzeit/webpack-isomorphic-tools
var WebpackIsomorphicTools = require('webpack-isomorphic-tools');
global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../build/webpack-isomorphic-tools'))
  .development(__DEVELOPMENT__)
  .server(rootDir, function() {
    require('../src/server');
  });

This script sets some globals for our server and includes the web pack-ismorphic-tools to our server. Those tools are necessary to execute our web pack configuration also on server side. But how does this work?
We add the Webpack-Iso-Tools to our normal web pack configuration to collect all assets which are included in our JS scripts (like sass files or images):
/build/webpack.config.js

// https://github.com/halt-hammerzeit/webpack-isomorphic-tools
var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
var webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools'));

if (__DEV__) {
  // ...
  // do not add in development mode! 
} else if (__PROD__) {
  // add web pack configuration to plugins
  webpackConfig.plugins.push(
    webpackIsomorphicToolsPlugin
  )
}

Create the webpack file for server side: build/webpack-isomorphic-tools.js

var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');

// see this link for more info on what all of this means
// https://github.com/halt-hammerzeit/webpack-isomorphic-tools
module.exports = {

  // when adding "js" extension to asset types
  // and then enabling debug mode, it may cause a weird error:
  //
  // [0] npm run start-prod exited with code 1
  // Sending SIGTERM to other processes..
  //
  // debug: true,

  assets: {
    images: {
      extensions: [
        'jpeg',
        'jpg',
        'png',
        'gif'
      ],
      parser: WebpackIsomorphicToolsPlugin.url_loader_parser
    },
    fonts: {
      extensions: [
        'woff',
        'woff2',
        'ttf',
        'eot'
      ],
      parser: WebpackIsomorphicToolsPlugin.url_loader_parser
    },
    svg: {
      extension: 'svg',
      parser: WebpackIsomorphicToolsPlugin.url_loader_parser
    },
    // this whole "bootstrap" asset type is only used once in development mode.
    // the only place it's used is the Html.js file
    // where a <style/> tag is created with the contents of the
    // './src/theme/bootstrap.config.js' file.
    // (the aforementioned <style/> tag can reduce the white flash
    //  when refreshing page in development mode)
    //
    // hooking into 'js' extension require()s isn't the best solution
    // and I'm leaving this comment here in case anyone finds a better idea.
    style_modules: {
      extensions: ['scss'],
      filter: function(module, regex, options, log) {
        if (options.development) {
          // in development mode there's webpack "style-loader",
          // so the module.name is not equal to module.name
          return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
        } else {
          // in production mode there's no webpack "style-loader",
          // so the module.name will be equal to the asset path
          return regex.test(module.name);
        }
      },
      path: function(module, options, log) {
        if (options.development) {
          // in development mode there's webpack "style-loader",
          // so the module.name is not equal to module.name
          return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
        } else {
          // in production mode there's no webpack "style-loader",
          // so the module.name will be equal to the asset path
          return module.name;
        }
      },
      parser: function(module, options, log) {
        if (options.development) {
          return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
        } else {
          // in production mode there's Extract Text Loader which extracts CSS text away
          return module.source;
        }
      }
    }
  }
}

The script also includes an own Babel file which we allows us to use ES6/7 in our application. We just have to create this file in our root directory: /server.babel.js

//  enable runtime transpilation to use ES6/7 in node

var fs = require('fs');

var babelrc = fs.readFileSync('./.babelrc');
var config;

try {
  config = JSON.parse(babelrc);
} catch (err) {
  console.error('==>     ERROR: Error parsing your .babelrc.');
  console.error(err);
}

require('babel-register')(config);

 

Unfortunately the server will not throw the error “Error: Error parsing your .babelrc” because in our starter kit there are comments which are not allowed here. To solve this error, just remove all comments from the /.babelrc file:

{
  "presets": ["es2015", "react", "stage-0"],
  "plugins": ["transform-runtime"]
}

 

The last part is to start our created executable file:
Modify the package.json file and add these items to the existing nodes:

{
  // ...
  "scripts": {
    "server": "better-npm-run server",
    // ...
  },
  "betterScripts": {
    "server": {
      "command": "nodemon --exec babel-node bin/production-server",
      "env": {
        "NODE_ENV": "development",
        "DEBUG": "app:*"
      }
    },
    // ....
  },
  // ...
}

 

Now we are really finished: start your script with

$ npm run deploy:prod
$ npm run server

 

Thank you for reading this tutorial. If you notice any mistake or language error, please fell free to contact me.

This tutorial is inspirited by:

 

Troubleshooting

  • Error: Cannot find module ‘layouts/CoreLayout/CoreLayout’
    If you get this error, your web pack added your source folder as a further root. Unfortunately this is not possible (I still did not find any solution for this) on server side rendering. Therefore you have to make those paths relative again:
    /src/routes/index.js:

    // ...
    import CoreLayout from '../layouts/CoreLayout/CoreLayout'
    import HomeView from '../views/HomeView/HomeView'
    // ...

     

Leave a Reply

Your email address will not be published. Required fields are marked *