Tutorial

Using React Router 4 with Server-Side Rendering

Published on June 4, 2018
Default avatar

By Alligator.io

Using React Router 4 with Server-Side Rendering

This tutorial is out of date and no longer maintained.

Introduction

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:

  1. yarn add react-router-dom

Or, using npm:

  1. npm install react-router-dom

And next, we’ll set up 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>
  );
};

Note: 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 an 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:

  1. yarn add isomorphic-fetch serialize-javascript

Or, using npm:

  1. 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 to 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 are 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 a 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';

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

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

Conclusion

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.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Alligator.io

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
4 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

came across the problem : “Invariant failed: You should not use <Switch> outside a <Router>”

Hey ,I am getting this error

(node:16976) UnhandledPromiseRejectionWarning: ReferenceError: window is not defined
    at Productpage.render (webpack:///./src/components/productpage/Productpage.component.js?:167:12)
    at processChild (D:\honestgadgets\node_modules\react-dom\cjs\react-dom-server.node.development.js:3134:18)
    at resolve (D:\honestgadgets\node_modules\react-dom\cjs\react-dom-server.node.development.js:2960:5)
    at ReactDOMServerRenderer.render (D:\honestgadgets\node_modules\react-dom\cjs\react-dom-server.node.development.js:3435:22)
    at ReactDOMServerRenderer.read (D:\honestgadgets\node_modules\react-dom\cjs\react-dom-server.node.development.js:3373:29)
    at Object.renderToString (D:\honestgadgets\node_modules\react-dom\cjs\react-dom-server.node.development.js:3988:27)
    at eval (webpack:///./server/server.js?:51:73)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:16976) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:16976) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

css is not working

import “./App.css”;

What is the content supposed to be in for the posts and todos components?

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel