ReactJS : Server side rendering with router v4 & redux

10 Jul 2017

This is post is originally published on crypt.codemancers.com.


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

  • Declarative routing
  • No more central routes config
  • Separate packages for web & native
  • No onEnter or onChange hooks/callbacks, instead use component lifecycle hooks

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

If you particularly enjoy my work, I appreciate donations given with Gittip.
comments powered by Disqus