Using React Router 4 with Server-Side Rendering

Now that we’ve had a look at a basic setup for React server-side rendering (SSR), let’s crank things up a notch and look at how to use React Router v4 on both the client and the server. After all, most real apps need routing, so it only makes sense to learn about setting up routing so that it works with server-side rendering.

Basic Setup

We’ll start things where we left things up in our intro to React SSR, but on top of that setup we’ll also need to add React Router 4 to our project:

$ yarn add react-router-dom

# or, using npm
$ npm install react-router-dom

And next we’ll setup a simple routing scenario where our components are static and don’t need to go fetch external data. We’ll then build on that to see how we would set things up for routes that do some data fetching on rendering.

On the client-side, let’s simply wrap our App component with React Router’s BrowserRouter component, as usual:

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';

import App from './App';

ReactDOM.hydrate(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

And then on the server we’ll use the analogous, but stateless StaticRouter component:

server/index.js

import React from 'react';
import express from 'express';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';

// ...other imports and Express config

app.get('/*', (req, res) => {
  const context = {};
  const app = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  const indexFile = path.resolve('./build/index.html');
  fs.readFile(indexFile, 'utf8', (err, data) => {
    if (err) {
      console.error('Something went wrong:', err);
      return res.status(500).send('Oops, better luck next time!');
    }

    return res.send(
      data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
    );
  });
});

app.listen(PORT, () => {
  console.log(`😎 Server is listening on port ${PORT}`);
});

The StaticRouter component expects a location and a context prop. We pass the current url (Express’ req.url) to the location prop and an empty object to the context prop. The context object is useful to store information about a specific route render, and that information is then made available to the component in the form of a staticContext prop.


To test that everything is working as we would expect, let’s add some routes to our App component:

src/App.js

import React from 'react';
import { Route, Switch, NavLink } from 'react-router-dom';
import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';

export default props => {
  return (
    <div>
      <ul>
        <li>
          <NavLink to="/">Home</NavLink>
        </li>
        <li>
          <NavLink to="/todos">Todos</NavLink>
        </li>
        <li>
          <NavLink to="/posts">Posts</NavLink>
        </li>
      </ul>

      <Switch>
        <Route
          exact
          path="/"
          render={props => <Home name="Alligator.io" {...props} />}
        />
        <Route path="/todos" component={Todos} />
        <Route path="/posts" component={Posts} />
        <Route component={NotFound} />
      </Switch>
    </div>
  );
};

We're making use of the Switch component to render only one matching route.

Now if you test out this setup ($ yarn run dev), you’ll see that everything is working as expected and our routes are being server-side rendered.

Serving NotFound using a 404 status

We can improve on things a little bit and serve the content with a HTTP status code of 404 when rendering the NotFound component. First, here’s how we can attach some data to the staticContext in the NotFound component:

src/NotFound.js

import React from 'react';

export default ({ staticContext = {} }) => {
  staticContext.status = 404;
  return <h1>Oops, nothing here!</h1>;
};

Then, on the server, we can check for a status of 404 on the context object and serve the file with a status of 404 if our check evaluates to true:

server/index.js

// ...

app.get('/*', (req, res) => {
  const context = {};
  const app = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  const indexFile = path.resolve('./build/index.html');
  fs.readFile(indexFile, 'utf8', (err, data) => {
    if (err) {
      console.error('Something went wrong:', err);
      return res.status(500).send('Oops, better luck next time!');
    }

    if (context.status === 404) {
      res.status(404);
    }

    return res.send(
      data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
    );
  });
});

// ...

Redirects

As a side note, you can do something similar to deal with redirects. React Router automatically adds an url property with the redirected url to the context object when a Redirect component is used:

server/index.js (partial)

if (context.url) {
  return res.redirect(301, context.url);
}

Loading Data

In the case where some of our app’s routes need to load data upon rendering, we’ll need a static way to define our routes instead of the dynamic way of doing it when only the client is involved. Losing the ability to define dynamic routes is one reason why server-side rendering is best kept for apps that really need it.


Since we’ll be using fetch on both the client and the server, let’s add isomorphic-fetch to the project. We’ll also add the serialize-javascript package, which will be handy to serialize our fetched data on the server:

$ yarn add isomorphic-fetch serialize-javascript

# or, using npm:
$ npm install isomorphic-fetch serialize-javascript

Let’s define our routes as a static array in a routes.js file:

src/routes.js

import App from './App';
import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';

import loadData from './helpers/loadData';

const Routes = [
  {
    path: '/',
    exact: true,
    component: Home
  },
  {
    path: '/posts',
    component: Posts,
    loadData: () => loadData('posts')
  },
  {
    path: '/todos',
    component: Todos,
    loadData: () => loadData('todos')
  },
  {
    component: NotFound
  }
];

export default Routes;

Some of our routes now have a loadData key that points to a function that calls a loadData function. Here’s our implementation for loadData:

helpers/loadData.js

import 'isomorphic-fetch';

export default resourceType => {
  return fetch(`https://jsonplaceholder.typicode.com/${resourceType}`)
    .then(res => {
      return res.json();
    })
    .then(data => {
      // only keep 10 first results
      return data.filter((_, idx) => idx < 10);
    });
};

We’re simply using the fetch API to get some data from a REST API.

On the server, we’ll make use of React Router’s matchPath to find the current route and see if it has a loadData property. If that’s the case, we call loadData to get the data and add it to the server’s response using a variable attached to the global window object:

server/index.js

import React from 'react';
import express from 'express';
import ReactDOMServer from 'react-dom/server';
import path from 'path';
import fs from 'fs';
import serialize from 'serialize-javascript';
import { StaticRouter, matchPath } from 'react-router-dom';
import Routes from '../src/routes';

import App from '../src/App';

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

app.use(express.static('./build'));

app.get('/*', (req, res) => {
  const currentRoute =
    Routes.find(route => matchPath(req.url, route)) || {};
  let promise;

  if (currentRoute.loadData) {
    promise = currentRoute.loadData();
  } else {
    promise = Promise.resolve(null);
  }

  promise.then(data => {
    // Let's add the data to the context
    const context = { data };

    const app = ReactDOMServer.renderToString(
      <StaticRouter location={req.url} context={context}>
        <App />
      </StaticRouter>
    );

    const indexFile = path.resolve('./build/index.html');
    fs.readFile(indexFile, 'utf8', (err, indexData) => {
      if (err) {
        console.error('Something went wrong:', err);
        return res.status(500).send('Oops, better luck next time!');
      }

      if (context.status === 404) {
        res.status(404);
      }
      if (context.url) {
        return res.redirect(301, context.url);
      }

      return res.send(
        indexData
          .replace('<div id="root"></div>', `<div id="root">${app}</div>`)
          .replace(
            '</body>',
            `<script>window.__ROUTE_DATA__ = ${serialize(data)}</script></body>`
          )
      );
    });
  });
});

app.listen(PORT, () => {
  console.log(`😎 Server is listening on port ${PORT}`);
});

Notice how we now also add the component’s loaded data to the context object. We’ll access this from staticContext when rendering on the server.

Now, in our components that need to fetch data on load, we can add some simple logic their constructor and their componentDidMount lifecycle method:

Here’s an example with our Todos component:

src/Todos.js

import React from 'react';
import loadData from './helpers/loadData';

class Todos extends React.Component {
  constructor(props) {
    super(props);

    if (props.staticContext && props.staticContext.data) {
      this.state = {
        data: props.staticContext.data
      };
    } else {
      this.state = {
        data: []
      };
    }
  }

  componentDidMount() {
    setTimeout(() => {
      if (window.__ROUTE_DATA__) {
        this.setState({
          data: window.__ROUTE_DATA__
        });
        delete window.__ROUTE_DATA__;
      } else {
        loadData('todos').then(data => {
          this.setState({
            data
          });
        });
      }
    }, 0);
  }

  render() {
    const { data } = this.state;
    return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>;
  }
}

export default Todos;

When rendering on the server, we can access the data from props.staticContext.data because we’ve put in into StaticBrowser’s context object.

There’s a little bit more logic going on with the componentDidMount method. Remember that this method is only called on the client. If __ROUTE_DATA__ is set on the global window object it means that we’re rehydrating after a server render and we can get the data directly from __ROUTE_DATA__ and then delete it. If __ROUTE_DATA__ is not set, then we arrived on that route using client-side routing, the server is not involved at all and we need to go ahead and fetch the data.

Another interesting thing here is the use of a setTimeout with a delay value of 0ms. This is just so that we can for the next JavaScript tick to ensure that __ROUTE_DATA__ is available.

React Router Config

There’s a package available and maintained by the React Router team, React Router Config, that provides two utilities to make dealing with React Router and SSR much easier: matchRoutes and renderRoutes.

matchRoutes

The routes in our previous example are quite simplistic and there’s no nested routes. In cases where multiple routes may be rendered at the same time, using matchPath won’t work because it’ll only match one route. matchRoutes is an utility that helps match multiple possible routes.

That means that we can instead fill an array with promises for matching routes and then call Promise.all on all matching routes to resolve the loadData promise of each matching route.

Something a little bit like this:

import { matchRoutes } from 'react-router-config';

// ...

const matchingRoutes = matchRoutes(Routes, req.url);

let promises = [];

matchingRoutes.forEach(route => {
  if (route.loadData) {
    promises.push(route.loadData());
  }
});

Promise.all(promises).then(dataArr => {
  // render our app, do something with dataArr, send response
});

// ...

renderRoutes

The renderRoutes utility takes-in our static route configuration object and returns the needed Route components. renderRoutes should be used in order for matchRoutes to work properly.

So with renderRoutes our App component changes to this simpler version instead:

src/App.js

import React from 'react';
import { renderRoutes } from 'react-router-config';
import { Switch, NavLink } from 'react-router-dom';

import Routes from './routes';

import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';

export default props => {
  return (
    <div>
      {/* ... */}

      <Switch>
        {renderRoutes(Routes)}
      </Switch>
    </div>
  );
};

👉 If you ever need a good reference for what we did here, have a look at the Server Rendering section of the React Router docs.

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#