Server-side rendering with create-react-app, React Router v4, Helmet, Redux, and Thunk

The Problem

We all know that server-side rendering is no longer essential for most major search engines. But if your development needs demand that your single page app (with multiple pages) work on Facebook and Twitter… prepare to ditch that nice serverless S3 setup you had going so well for you — it’s time to go back to the server. My problem was that if you tried to access the single page app on Facebook it would not grab any of the Open Graph <meta> tags it needs to parse.

Well why don’t you just put the <meta> tags in the public HTML file and be done with it?

What if you would like to have public profile pages uniquely visible to be shared on social media platforms? I had a client request just this very thing the other day. They wanted public profile pages for their users (just like any other social media site probably would) to show up in Facebook with relevant data to that profile — not some “catch all” meta tags that all pages share.

But don’t I have to eject?

Absolutely not, silly goose!

I’ve built an example application that I’ve deployed to Heroku. Of course, thanks to the simplicity of create-react-app, our pages render in pristine beauty. In my example, we have a homepage, an about page, and a not found route to resolve all bad links. I also added a sample notification reducer as a proof of concept that Redux and Thunk are working correctly. The purpose of this post isn’t to establish how Redux works… there’s plenty of tutorials out there for you.

How lovely. All three pages rendering in perfect synchrony.

Being the terrific engineers that we are, we’d love for Facebook to just play along with our silly games. Facebook’s scraper is not always so kind.

Facebook crawl errors… for days.

Facebook doesn’t find our tags.

But I added them to react-helmet just like I should!

Yes you did, you fool! We can see that they even show up in the DOM.

The plot thickens…

I’ve included my react-helmet wrapper in this project (borrowed from Ryan Glover, which I’ve further modified to suit my needs). The wrapper that I’m using allows for a really easy syntax with defaults you can fall back on provided you don’t supply all the information. The syntax is roughly like such:

<Page title="About" description="This page will have a custom set description" id="about">
<h1>We have an about page</h1>
<p>It's full of fun surprises.</p>
</Page>

The Solution

Great, so Facebook doesn’t care about our silly, little app. Although the user experience on the site is perfect (users see updated titles, they see the updated social media tags… blah blah blah), Facebook still isn’t happy.

Enter server-side rendering.

Goals

  1. I don’t want to change anything on create-react-app, at all.
  2. I want the server-side rendering to work for me and not have to be maintained.
  3. I want the Nashville Predators to be posthumously awarded the Stanley Cup.

The Code

TL;DR: Here’s the Github Gist you can drop into your project.

I have everything silo’d off into a server folder sitting at the base of my application. In React app, we have all of our Babel nonsense taken care of for us with a constant watch on the src directory. We want that same nonsense applied to our server. We start with an entry point to kick off Babel:

// Ignore those pesky styles
require('ignore-styles');

// Set up babel to do its thing... env for the latest toys, react-app for CRA
require('babel-register')({
ignore: /\/(build|node_modules)\//,
presets: ['env', 'react-app']
});

// Now that the nonsense is over... load up the server entry point
require('./server');

The last thing we do is we load in our server.js file, which is the real entry point to our server. Here’s what that looks like:

import bodyParser from 'body-parser';
import compression from 'compression';
import express from 'express';
import morgan from 'morgan';
import path from 'path';

import index from './routes-index';
import api from './routes-api';
import universalLoader from './universal';

// Create our express app (using the port optionally specified)
const app = express();
const PORT = process.env.PORT || 3000;

// Compress, parse, and log
app.use(compression());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(morgan('dev'));

// Set up route handling, include static assets and an optional API
app.use('/', index);
app.use(express.static(path.resolve(__dirname, '../build')));
app.use('/api', api);
app.use('/', universalLoader);

// Let's rock
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}!`);
});

// Handle the bugs somehow
app.on('error', error => {
if (error.syscall !== 'listen') {
throw error;
}

const bind = typeof PORT === 'string' ? 'Pipe ' + PORT : 'Port ' + PORT;

switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
});

And our index route…

// Any route that comes in, send it to the universalLoader

import express from 'express';
import universalLoader from '../universal';

const router = express.Router();

router.get('/', universalLoader);

export default router;

And an optional API route (which we don’t need, but why not?)…

// This file includes an optional API common in isomorphic applications
// Of course, you should probably spin up your API elsewhere... but you get the idea

import express from 'express';

const router = express.Router();

router.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});

router.get('/', (req, res, next) => {
res.json({});
});

export default router;

Basically all we’re doing is passing every route on to the universal loader (to give credit where credit is due, thanks to Ben Lu for his brilliant work). That’s where the magic is…

import path from 'path';
import fs from 'fs';

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

import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';
import { Route } from 'react-router-dom';
import createServerStore from './store';

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

// A simple helper function to prepare the HTML markup
const prepHTML = (data, { html, head, body }) => {
data = data.replace('<html lang="en">', `<html ${html}`);
data = data.replace('</head>', `${head}</head>`);
data = data.replace('<div id="root"></div>', `<div id="root">${body}</div>`);

return data;
};

const universalLoader = (req, res) => {
// Load in our HTML file from our build
const filePath = path.resolve(__dirname, '../build/index.html');

fs.readFile(filePath, 'utf8', (err, htmlData) => {
// If there's an error... serve up something nasty
if (err) {
console.error('Read error', err);

return res.status(404).end();
}

// Create a store and sense of history based on the current path
const { store, history } = createServerStore(req.path);

// Render App in React
const routeMarkup = renderToString(
<Provider store={store}>
<ConnectedRouter history={history}>
<Route component={App} />
</ConnectedRouter>
</Provider>
);

// Let Helmet know to insert the right tags
const helmet = Helmet.renderStatic();

// Form the final HTML response
const html = prepHTML(htmlData, {
html: helmet.htmlAttributes.toString(),
head:
helmet.title.toString() +
helmet.meta.toString() +
helmet.link.toString(),
body: routeMarkup
});

// Up, up, and away...
res.send(html);
});
};

export default universalLoader;

I’ve modified what Ben Lu did here pretty significantly. Here’s the changes:

  1. I used ConnectedRouter instead of StaticRouter. Remember that BrowserRouter goes on the client and StaticRouter goes on the server. In this case, I wanted to use the bleeding edge version of React-Redux-Router, 5.0.0 (alpha something-rather). They use ConnectedRouter, which can be used on both the client and the server. How lovely.
  2. We use react-helmet here (as mentioned above), which allows for a really declarative way of defining <head> modification. The issue is that react-helmet doesn’t know to update the tags on the server-side. This causes us to call Helmet.renderStatic() which will give us what we need in object form.
  3. I wrote a HTML prep function which just inserts the <html>, <head>, and <body> information in the proper places. You can modify this to your liking. The way it’s written will leave whatever else you’ve placed in your <head> alone, as well as your <body>. Again, this is just what I needed… feel free to hack this to fit your needs.

Once all is done, we send off the response. Voila!

It wouldn’t be good if we didn’t prove this works. Let’s go back to the Facebook debugger and scrape it again:

Note the overrides being done for title and meta description… how sexy.

The only thing remaining is that you have to run the build function before you deploy. I’m using Yarn, so for me, I’ll run the following:

# Development
yarn build
yarn serve
# Production
yarn build
yarn deploy

Some Restrictions

  1. No code splitting. For shame.
  2. You can’t use document or window in your React application except in componentDidMount and componentWillUnmount. This is because servers don’t have access to the DOM, and therefore there’s no such thing as document or window. Not that big of a deal… just something to be aware of.

Although this isn’t a restriction: I don’t currently have any <Redirect> routes in my application — I’m sure that could be done somewhat simply, but for purposes of this demo… I didn’t include it here.

Conclusions

There’s plenty of improvement that can be done I’m sure. Hit me up on Twitter (or in the comments below) for questions and comments.

Link to the full example: https://github.com/cereallarceny/cra-ssr
A link to the Gist you can drop in to any create-react-app: https://gist.github.com/cereallarceny/e5bee7cb95ddfe4958f86d6bcda49ae8

There’s also a PR for this on the 5.0.0 version of react-router-redux: https://github.com/ReactTraining/react-router/pull/5614

Server-side rendering does not have to be a scary monster.