How to add server side rendering to React, Redux and React Router V4 Application

Single page applications give you freedom to implement complex User Interfaces and also helps in boosting the overall performance of the websites. I am going to assume that you guys already know how single page applications ( SPA’s ) work in web browsers so here is quick overview of how things work in SPA’s.

When a user opens a page our SPA in browser, The server responds with a very simple bare bone almost empty html page with not much markup in the body. The job of this page is to load the javascript files of our application which are responsible for starting our SPA. All the heavy lifting happens in the javascript code.

There are some advantages as well as disadvantages of Single Page Applications. One of the advantage of SPA is that our website performance increases in most cases and the page load time decreases.
The main disadvantage of SPA’s is that these websites are not as search engine friendly as the traditional websites are. Since the content is generated on the client side most of the Search Engines including Google index the pages but most likely they miss some parts of the page.

While this might not be a big deal for most websites but the sites that depend of search engine organic traffic can not afford to have pages partially indexed without all the data. For that we use a technique call Server Side Rendering ( SSR ) where we generate the content of our SPA on the server side and send it to browser. Now when the pages loads in the user browser it has all the bits and pieces of the page. After the initial load the javascript takes over the application and the applications starts acting like a normal SPA would.

To add SSR support to our React Redux applications we have to follow certain guidelines.

Routes

Routes need to defined in different way if you want to do SSR. Normally you define routes like so

<Router>
 <Route path="/contact" component={Contact} />
</Router>

All you have to do is defines all your routes in an array. The above route will look something like this

const routes = [
 {
  path: '/contact',
  component: Contact,
  fetchData: function(match, dispatch){
   return getMyContactPageDynamicData().then(res => {
    dispatch({
     type: CONTACT_PAGE_DATA_LOADED,
     payload: res.data
    });
   });
  }
 }
];

As you can see one object in the routes array represents a single route and the Route component props are translated to the route object keys like path and component with one exception and that is the fetchData method. We have defined this method on the route object and we will call this method when that particular route matches in our case when the /contact page is opened.

Normally we use routes in our react-router applications like this

<div id="app-container">
 <Router>
  <Route path="/contact" component={Contact} />
 </Router>
</div>

Now that we have an array of routes we need to install a package called react-router-config to convert our array routes to normal react router compatible routes. react-router-config comes with a method called renderRoutes which takes the array routes and returns the normal routes that can be used in our application. react-router-config also comes with another function called matchRoutes which is used on the match the routes based on the current request url. Her is an example usage of renderRoutes

import React from 'react';
import { renderRoutes } from 'react-router-config';
import routes from './routes';
.
.
.
render(){
 return (
  <div id="app-container">
   {renderRoutes(routes)}
  </div>
 );
}

Now that we have taken care of the client side part lets move on to the server side where we will use the fetchData function to load data from the server and generate the html markup for our application.

I am using express on the server side for server side rendering, you are free to use any framework or library you like. This is what my server looks like

import express from 'express';
import axios from 'axios';
import ReactDOMServer from 'react-dom/server';
import { matchRoutes } from 'react-router-config';
import configureStore from '../configureStore'; // For redux store
.
.
.
app.get('*', (req, res, next) => {
  let htmlMarkup = '<html><body><div id="root"></div><script>window.__PRELOADED_STATE__ = {};</script></body></html>';
  try{
    const store = configureStore();
    let promises = [];
    const matchedRoutes = matchRoutes(routes, req.url);
    matchedRoutes.map(({route, match}) => {
     if(typeof route.fetchData == 'function'){
      promises.push(route.fetchData(match, store.dispatch));
     }
    });
    axios.all(promises).then(() => {
      const appMarkup = ReactDOMServer.renderToString(<App store={store} req={req} />);
      htmlMarkup = appHtml.replace('<div id="app-container"></div>', '<div id="app-container">'+ appMarkup +'</div>');
      let storeString = '{}';
      try{
       storeString = JSON.stringify(store.getState()).replace(/</g, '\\u003c');
      }catch(e){
       storeString = '{}';
      }
      htmlMarkup = htmlMarkup.replace('window.__PRELOADED_STATE__ = {};', 'window.__PRELOADED_STATE__ = '+ storeString +';');
      res.send(htmlMarkup);
    }).catch((err) => {
      next(err);
    });
  }catch(err){
    next(err);
  }
});

As you can see matchRoutes function provided by the react-router-config package is used to match the URL that was requested and it returns the matched route or set of routes in an array. After matching routes we loop over each matched route and check if the route has a method called fetchData exists, If fetchData exists we execute it and pass the match from matched route and dispatch from redux store.
Now that we have an array of all the promises we can use the axios.all(promises) method, When all the promises complete we are updating the appHtml with our appMarkup because now it has all the data it requires from the redux store. After preparing the htmlMarkup we simply return it to the client.

That’s it, You now have a server rendered react redux application that is SEO friendly. I would like to know your thoughts on this, If you have something to contribute please leave a comment.

Leave a Reply