ReactJS : Server side rendering with router v4 & redux

Revath S Kumar  - June 3, 2017  |  

When I wrote React.js: Server side rendering a few months back, I used react router v3.0.2. But ever since react router released v4, which is a total rewrite into a declarative format, the old blog post won’t work with react router v4. So I decided to write a new blog as 2nd part of it which uses react router v4 along with redux.

Since we already have a blog post explaining the initial setup, I will be skipping the repeated steps needed here but will add the new updates need to use the new router.

Major Router Updates

Major changes in React Router v4 are

Adding React Router v4 to our application

Since react router has separate packages for web and native, let go with installing the package needed for the web.

npm i --save react-router-dom react-router-config

react-router-config package will have configuration helpers to use with StaticRouter for server side rendering. If your application already has react-router package, I recommend you to remove it and use only the above ones.

Adding Redux to our application

Let add the redux packages need for our application. Since this demo contains async actions we will add redux-thunk package as well.

npm i --save redux react-redux redux-thunk

If you are not familiar with the redux setup you can follow my previous blog on Getting started with redux.

Also, let’s install isomorphic-fetch so we can use fetch on both server and client.

npm i --save isomorphic-fetch

Setup React Router

Setting up the Router will start with defining the routes.

// client/routes.js

import AppRoot from './app-root';
import Home from './home';
import List from './list';

const routes = [
  { component: AppRoot,
    routes: [
      { path: '/',
        exact: true,
        component: Home
      },
      { path: '/home',
        component: Home
      },
      { path: '/list',
        component: List
      }
    ]
  }
];

export default routes;

And load the routes on client side like

// client/app.jsx

import React from 'react';
import {render} from 'react-dom';

import BrowserRouter from 'react-router-dom/BrowserRouter';
import { renderRoutes } from 'react-router-config';

import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';

import routes from './routes';
import reducers from './modules';

const store = createStore(
  reducers, window.__INITIAL_STATE__, applyMiddleware(thunk)
);

const AppRouter = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        {renderRoutes(routes)}
      </BrowserRouter>
    </Provider>
  )
}

render(<AppRouter />, document.querySelector('#app'));

Here <BrowserRouter> is a new component provided by react router which uses HTML5 history API. The above setup is used only on client side. For server side rendering we will be using <StaticRouter> component.

Render static component on Server

As same as PART 1, we have a /home route which will render some HTML. No dynamic content or data from API. Even though our Home component is same, we have a different setup on routes/index.jsx.

// routes/index.jsx

import express from 'express';
import request from 'request';

import React from 'react';
import { renderToString } from 'react-dom/server';

import StaticRouter from 'react-router-dom/StaticRouter';
import { renderRoutes } from 'react-router-config';

import routes from '../client/routes';

const router = express.Router();

router.get('*', (req, res) => {
  let context = {};
  const content = renderToString(
    <StaticRouter location={req.url} context={context}>
      {renderRoutes(routes)}
    </StaticRouter>
  );
  res.render('index', {title: 'Express', data: false, content });
});

module.exports = router;

Render component with data

Now when it comes to rendering component with data, we need to make some changes to the express route, setup redux store and add static method on a component to fetch the data and update the store.

Since we are using redux we need to setup reducer & action to fetch the user details from API. Here I will be using erikras/ducks-modular-redux pattern, so the constants, reducer & actions will be available in a single file.

// client/modules/users.js

import 'isomorphic-fetch';

export const USERS_LOADED = '@ssr/users/loaded';

const initialState = {
  items: []
};

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case USERS_LOADED:
      return Object.assign({}, state, { items: action.items });
  
    default:
      return state;
  }
}

export const fetchUsers = () => (dispatch) => {
  return fetch('//jsonplaceholder.typicode.com/users')
    .then(res => {
      return res.json();
    })
    .then(users => {
      dispatch({
        type: USERS_LOADED,
        items: users
      });
    })
}

Now let’s modify the List component to use the fetchUsers action and also add a fetchData static method which can be used on the server.

// client/list.jsx

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import { fetchUsers } from './modules/users';

class List extends Component {
  static fetchData(store) {
    return store.dispatch(fetchUsers());
  }

  componentDidMount() {
    this.props.fetchUsers();
  }

  render() {
    return (
      <div >
        {
          this.props.items.map(item => {
            return (
              <div key={item.id} >
                <span>{item.name}</span>
              </div>
            )
          })
        }
      </div>
    );
  }
}

const mapStateToProps = (state) => ({items: state.users.items});
const mapDispatchToProps = (dispatch) => 
  bindActionCreators({ fetchUsers }, dispatch);

export default connect(mapStateToProps, mapDispatchToProps)(List);

Now the updates for the express route.

// routes/index.jsx

import express from 'express';
import request from 'request';

import React, {Component} from 'react';
import {renderToString} from 'react-dom/server';

import StaticRouter from 'react-router-dom/StaticRouter';
import { matchRoutes, renderRoutes } from 'react-router-config';

import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';

import routes from '../client/routes';
import reducers from '../client/modules';

/*eslint-disable*/
const router = express.Router();
/*eslint-enable*/

const store = createStore(reducers, applyMiddleware(thunk));

router.get('*', (req, res) => {
  const branch = matchRoutes(routes, req.url);
  const promises = branch.map(({route}) => {
    let fetchData = route.component.fetchData;
    return fetchData instanceof Function ? fetchData(store) : Promise.resolve(null)
  });
  return Promise.all(promises).then((data) => {
    let context = {};
    const content = renderToString(
      <Provider store={store}>
        <StaticRouter location={req.url} context={context}>
          {renderRoutes(routes)}
        </StaticRouter>
      </Provider>
    );
    res.render('index', {title: 'Express', data: store.getState(), content });
  });
});

module.exports = router;

In the above snippet, matchRoutes will filter the routes and components needed to render the given URL. Once we have the list of routes for the given URL, we can map through each and check whether it has a static method named fetchData. If the component has the fetchData method, then execute those else return a null promise.

Once we collect all the promises, executed and updated the store, we can render the component using <StaticRouter> component and return the data and compiled HTML to the client.

Now when we navigate to /list, the route we can see the list of users rendered from the server.

Handling 404

Next, let’s see how to handle the 404. In this case just rendering the NotFound component is not enough, we have to return back appropriate status code to the client as well.

Let’s start with adding NotFound component

// client/notfound.jsx

import React from 'react';
import { Route } from 'react-router-dom';

const NotFound = () => {
  return (
    <Route render={({ staticContext }) => {
      if (staticContext) {
        staticContext.status = 404;
      }
      return (
        <div>
          <h1>404 : Not Found</h1>
        </div>
      )
    }}/>
  );
};

export default NotFound;

In NotFound component, rendering some 404 message is not enough. We should be setting the status on staticContext so that when rendering on the server we can access the status on the context object we passed.

Remember staticContext will be available only on the server, so make sure we guard the setting of status with if condition.

next, we add the route to handle 404.

// client/routes.js

import AppRoot from './app-root';
import Home from './home';
import List from './list';
+import NotFound from './notfound';

const routes = [
  { component: AppRoot,
    routes: [
     { path: '/',
        exact: true,
        component: Home
      },
      { path: '/home',
        component: Home
      },
      { path: '/list',
        component: List
      }
+     {
+       path: '*',
+       component: NotFound
+     }
    ]
  }
];

export default routes;

Now we need to update the express routes to set the response status as 404.

// routes/index.jsx

import express from 'express';
import request from 'request';

import React, {Component} from 'react';
import {renderToString} from 'react-dom/server';

import StaticRouter from 'react-router-dom/StaticRouter';
import { matchRoutes, renderRoutes } from 'react-router-config';

import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';

import routes from '../client/routes';
import reducers from '../client/modules';

const router = express.Router();

const store = createStore(reducers, applyMiddleware(thunk));

router.get('*', (req, res) => {
  const branch = matchRoutes(routes, req.url);
  const promises = branch.map(({route}) => {
    let fetchData = route.component.fetchData;
    return fetchData instanceof Function ? fetchData(store) : Promise.resolve(null)
  });
  return Promise.all(promises).then((data) => {
    let context = {};
    const content = renderToString(
      <Provider store={store}>
        <StaticRouter location={req.url} context={context}>
          {renderRoutes(routes)}
        </StaticRouter>
      </Provider>
    );
+   if(context.status === 404) {
+     res.status(404);
+   }
    res.render('index', {title: 'Express', data: store.getState(), content });
  });
});

module.exports = router;

Handling Redirects

After handling 404, now we can handle redirects in a similar way. For redirects, we will be using <Redirect> component from react router. To show the redirection we will be redirecting /list route to a new route /users where we will list the users from API.

For this, we will define a new component ListToUsers which utilises <Redirect>.

// client/listtousers.jsx

import React from 'react';
import { Route, Redirect } from 'react-router-dom';

const ListToUsers = () => {
  return (
    <Route render={({ staticContext }) => {
      if (staticContext) {
        staticContext.status = 302;
      }
      return <Redirect from="/list" to="/users" />
    }}/>
  );
};

export default ListToUsers;

As we did in handling 404, here as well we need to set the status on staticContext to 302 or 301 as per your need. Here I am using 302.

Now let’s update the routes.

// client/routes.js

import AppRoot from './app-root';
import Home from './home';
import List from './list';
import NotFound from './notfound';
+import ListToUsers from './listtousers';

const routes = [
  { component: AppRoot,
    routes: [
     { path: '/',
        exact: true,
        component: Home
      },
      { path: '/home',
        component: Home
      },
+     { path: '/list',
+       component: ListToUsers
+     }
+     { path: '/users',
+       component: List
+     }
      {
        path: '*',
        component: NotFound
      }
    ]
  }
];

export default routes;

Next, make necessary changes for express routes so it will perform redirect

// routes/index.jsx

// All neeeded imports

router.get('*', (req, res) => {
  const branch = matchRoutes(routes, req.url);
  const promises = branch.map(({route}) => {
    let fetchData = route.component.fetchData;
    return fetchData instanceof Function ? fetchData(store) : Promise.resolve(null)
  });
  return Promise.all(promises).then((data) => {
    
    // render component to string

+   if (context.status === 302) {
+     return res.redirect(302, context.url);
+   }
    res.render('index', {title: 'Express', data: store.getState(), content });
  });
});

module.exports = router;

Now we have a fully functional server rendered react application.

The demo app is available on github and working demo can be found in now.sh

React.js : Server side rendering

Revath S Kumar  - September 16, 2016  |  

These days server side rendering has become an important feature for heavy client side applications and now most of the client side frameworks support it. Yesterday I tried a bit of react server rendering along with express.js and react-router.

Setup Express js server and dependencies.

We can start with scaffolding a new express.js app using expressjs-generator and installing all the dependencies. We use webpack for client side bundling.

We can install all the dependencies by running

npm i --save react react-dom react-router babel-register

and development dependencies by running

npm i --save-dev babel-cli babel-core babel-preset-es2015 babel-preset-react babel-preset-stage-0 webpack babel-loader

Setup webpack & babel for client.

We will keep all our client JavaScript and React components in a new folder named client and put all the compiled js in public/javascripts. Also we shall add a .babelrc to load babel presets and configs.

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

Now in webpack.config.js, we will configure the entry point, output directory and babel loader.

// webpack.config.js

var path = require('path');

module.exports = {
  entry: './client/app.jsx',

  output: {
    filename: 'app.js',
    path: path.join('public/javascripts/')
  },

  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        query: {
          presets: ['react', 'es2015']
        }
      }
    ]
  }
};

Now we can run the following to build client JavaScript files in development mode with debug flag turned on, and watch for changes.

webpack --debug --watch

For easier use, we can add the command to npm scripts with name webpack:server. Now we just need to run npm run webpack:server.

Setup react router

Now the basic scaffolding and development setup is finished and time to start building our app. We can start with configuring the router. We are planning mainly for two routes /home to show the rendering of static component and /list to show the server side rendering with some data.

First we have to define the entry point which will mount our react-router component to DOM.

// client/app.jsx

import React from 'react';
import {render} from 'react-dom';

import AppRouter from './router.jsx';

render(<AppRouter/>, document.querySelector('#app'));

Next, define routes in client/router.jsx

// client/router.jsx

import React from 'react';
import {Router, browserHistory, Route} from 'react-router';

import AppRoot from './app-root.jsx';
import Home from './home.jsx';
import List from './list.jsx';

const AppRouter = () => {
  return (
    <Router history={browserHistory}>
      <Route path="/" component={AppRoot}>
        <Route path="/home" component={Home}/>
        <Route path="/list" component={List}/>
      </Route>
    </Router>
  );
};

export default AppRouter;

AppRoot is nothing but a simple layout for our app.

// client/app-root.jsx

import React, {Component} from 'react';
import {Link} from 'react-router';

class AppRoot extends Component {
  render() {
    return (
      <div>
        <h2>React Universal App</h2>
        <Link to="/home"> Home </Link>
        <Link to="/list"> List </Link>
        {this.props.children}
      </div>
    );
  }
}

export default AppRoot;

Setup express js for server side rendering

Since we are using React + ES6 for components, we have to use the babel-register on server side so that we can write express js routes also in ES6 and import the react routes we already wrote. Please note that, we have to require/import the babel-register at the beginning of express js entry point app.js.

// app.js
require('babel-register');
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');

//rest of the express js boilerplate

Then we rename the routes/index.js to routes/index.jsx, after this we can use the react routes and react components on server side. For server side rendering we use renderToString method from react-dom/sever package and methods like match, createRoutes and RouterContext from react-router.

match function in react-router module will match a set of routes to a location and calls a callback, without rendering. We use createRoutes method from react-router to create a set of routes from our client/router.jsx(appRouter) component and provide it to match.

// routes/index.jsx
// express and react imports

import appRouter from '../client/router.jsx';

const routes = createRoutes(appRouter());

Once we have a match RouterContext will render the component tree for the given router state and return the component markup as a string with the help of renderToString method.

// Express.js route

router.get('*', (req, res) => {
  match({routes, location: req.url}, (error, redirectLocation, renderProps) => {
    // check for error and redirection
    const content = renderToString(<RouterContext {...renderProps}/>);
    // pass content to jade view (we'll see it in a while)
  })
})

Now we have the react components rendered as string and we need to pass this to our pug.js (Previously known as jade) view. The jade view will accept the string in content variable and substitute inside the react app mount point.

//- views/index.jade
extends layout

block content
  script(type='text/javascript').
    window.__INITIAL_STATE__ = !{JSON.stringify(data)}
  div.container#app!= content

Rendering a static component on server

/home points to a static component called Home which we are going to render from server.

import React from 'react';

const Home = () => {
  return (
    <div>
      <h1>Home</h1>
    </div>
  );
};

export default Home;

and now when we join the dots the routes/index.jsx will look like this

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {RouterContext, match, createRoutes} from 'react-router';

import appRouter from '../client/router.jsx';

const routes = createRoutes(appRouter());

const router = express.Router();

router.get('/home', (req, res) => {
  match({routes, location: req.url}, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message);
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (renderProps) {
      const content = renderToString(<RouterContext {...renderProps}/>);
      res.render('index', {title: 'Express', data: false, content});
    } else {
      res.status(404).send('Not Found');
    }
  });
});

Rendering component on server with data

In this section, we are trying to render a list of users, the data source is not DB but an API for demo purpose. In order to render data, we need to fetch data from the server, pass it to a component via context. For this we need to write a Higher Order Component to set the data to context.

import express from 'express';
import request from 'request';
import React, {Component} from 'react';
import {renderToString} from 'react-dom/server';
import {RouterContext, match, createRoutes} from 'react-router';

import appRouter from '../client/router.jsx';

const routes = createRoutes(appRouter());

class DataProvider extends Component {
  getChildContext() {
    return {data: this.props.data};
  }
  render() {
    return <RouterContext {...this.props}/>;
  }
}

DataProvider.propTypes = { 
  data: React.PropTypes.object
};

DataProvider.childContextTypes = { 
  data: React.PropTypes.object
};

The above DataProvider will set data to context if we pass it via props named data.

The List component will look like,

import React, {Component} from 'react';

class List extends Component {
  constructor(props, context) {
    super(props, context);
    this.state = this.context.data || window.__INITIAL_STATE__ || {items: []};
  }

  componentDidMount() {
    this.fetchList();
  }

  fetchList() {
    fetch('http://jsonplaceholder.typicode.com/users')
      .then(res => {
        return res.json();
      })  
      .then(data => {
        this.setState({
          items: data
        });
      })  
      .catch(err => {
        console.log(err);
      }); 
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item => {
          return <li key={item.id}>{item.name}</li>;
        })}
      </ul>
    );  
  }
}

List.contextTypes = { 
  data: React.PropTypes.object
};

export default List;

The above list component will look for data in context first, then in global state and later in component level state. While we render it from server, the data will be available in context and component use the data in context to render the initial HTML. Later after loading it in browser the component can fetch again and update the data.

Now we can setup the route to fetch the data and render the component.

router.get('/list', (req, res) => {
  match({routes, location: req.url}, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message);
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (renderProps) {
      request('http://jsonplaceholder.typicode.com/users', (error, response, body) => {
        const data = {items: JSON.parse(body)};
        const content = renderToString(<DataProvider {...renderProps} data={data}/>);
        res.render('index', {title: 'Express', data, content});
      });
    } else {
      res.status(404).send('Not Found');
    }
  });
});

Thats it. We successfully rendered our react components from server side with and without data, so that user don’t have to wait for another ajax request after loading the page to see the data.