Server-side rendering with create-react-app, code-splitting, preloaded data, React Router, Helmet, Redux, and Thunk

I feel we’ve been here a million times before. Your brand new web application is served hot and ready in the cloud. Component structure is well-organized. UI is on fleek. But Facebook’s sharing debugger throws shade.

I’ve spoken about this before. For a million and a half reasons you cannot seem to get social networks and some search engines to play nice and just respect your damn content. Here we are again, with the latest toys and a few new additions. If you want to read about why server-side rendering is necessary for many deployed applications, feel free to read my previous article. In this tutorial, I’ll mostly be hopping around showing off individual portions of how to accomplish the greater goal. If you want the full picture, head over to the Github repo. You can also view the final product in all it’s beautiful form.

I set out to create the best damn server-rendered create-react-app application possible. Easier said than done.

Goals

  1. Zero modifications to your existing CRA application
  2. Create React App without ejecting
  3. React 16 (fiber, baby!)
  4. React Router v4 (with Thunk, via Connected React Router)
  5. Full SEO support via React Helmet
  6. Preloaded page data via async/await and React Frontload
  7. Code splitting via React Loadable
  8. Server-side cookie support

Let’s roll through these for brevity sake.

Zero modifications: When I say “zero modifications”, I mean that if you want to render a React app on the server, all you “technically” need to is to add one folder with three files. If you want a few nice toys like preloaded page data and code splitting, we can do so with minimal additional effort.

React Fiber: We’re using the latest and greatest version of React: 16.4. This means access to hydrate and other sexy performance upgrades. Love it.

React Router: React Router allows us a super simple, declarative way of routing. This is tied directly to Redux via Connected React Router. Instead of using BrowserRouter, we use ConnectedRouter on the client-side and StaticRouter on the server.

Note: A former version of this post used React Router Redux v5, which has been deprecated in favor of Connected React Router. All code samples in this post, in the gist, and in the Github repo have been updated.

Full SEO support: I’ve written (semi-borrowed) a React component (<Page>) to wrap around any route and immediately render the appropriate meta and title tags for SEO. All content is optional, you can even set defaults for yourself.

Preloaded page data: With the help of Dave Williams, we’re using React Frontload, a library which helps you define functions to run before the server returns a response. This is perfect for user profile pages where the title, description, and images may be different from user to user. This teaches Facebook to shut up and wait a damn second for your response to come back. For purpose of demonstration, I’ve just created a 3-second timeout, but this would of course be substituted by an API call or some other asynchronous action.

Code splitting: Code splitting is done using named exports. Instead of some crazy random hash for your Javascript chunks, you get nicely readable file names. Purdy.

Cookie support: If you’re using cookies for storing user session or for any other task, you can have full access to them on the server as well. In my example, I use this to set the user state before the application has loaded. This prevents us from accidentally rendering the login page before some other login-protected route (like /dashboard for instance).

What to Expect

Multiple pages: You should be able to go to your homepage, about page, terms of service, privacy policy, contact page… all of them will have different titles and metadata which should accurately show on any search engine and social network.

User-specific pages: A user profile page nearly always has unique information to that page, even though the component and route structure is the same. You should be able to hit /profile/1 or /profile/2 and have them display unique metadata. Never again will Facebook render “Profile — This is a user’s profile page” regardless of the profile… or much worse, the default metadata from your homepage — yikes!

Authenticated pages: Technically speaking, no crawler of any kind should be picking up the metadata of authenticated content because the crawler isn’t logged in. However, it is quite handy to show a changed page title when you navigate through authenticated pages. We can also set these to not be crawled by search engines if we desire, and we should.

Not found pages: We always need a catch-all route. Let’s make sure all crawlers are able to pick this up and show it when appropriate.

Implementing the Server

Our server is comprised of three files:

  • index.js (which sets up Babel)
  • server.js (which starts the Express server)
  • loader.js (which is called on every page load to get the right data and render the right route)

They’re mostly pretty straightforward and I’ve done a pretty good job at documenting what’s going on. You can find them all in a gist located here, but I’ll go ahead and explain bits and pieces of the most important file: loader.js.

Before we do anything, we need to dispatch do a few things. First, let’s create a store via our current route passed from Express. This uses MemoryHistory instead of BrowserHistory like you might expect on the client-side. Second, we need to set the current user depending on the state of a cookie created on the client. Without these, we’d have no context over the state of the app when rendering our Redux object structure. We can do so using the following code:

const { store } = createStore(req.url);
if ('mywebsite' in req.cookies) {
store.dispatch(setCurrentUser(req.cookies.mywebsite));
} else {
store.dispatch(logoutUser());
}

Fairly simple, no?

With that out of the way — the most important block of code is where we do the actual rendering. Normally this is pretty obvious, but in our situation things are a bit more complicated:

frontloadServerRender(() =>
renderToString(
<Loadable.Capture report={m => modules.push(m)}>
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<Frontload isServer>
<App />
</Frontload>
</StaticRouter>
</Provider>
</Loadable.Capture>
)
).then(routeMarkup => {
// Do stuff with routeMarkup
});

Ay caramba! Here’s the core functionality of this file. We do the following in specific order (inside-out):

  1. Load the <App /> component
  2. Inside of the Frontload HOC
  3. Inside of a Redux <StaticRouter /> (since we’re on the server), given a location to receive and context object to write to
  4. Inside of a <Provider /> with the store we created earlier
  5. Inside of the React Loadable HOC to make sure we have the right scripts depending on page
  6. Render all of this sexiness like normal
  7. Wrap the render inside of a Frontload server render so it knows to get all the appropriate preloaded requests

In English, we basically need to know what page we’re dealing with, and then load all the appropriate scripts and data for that page. This is then loaded into the correct components and sent as a Promise to be handled below.

As for code splitting, we need to know what files to load and then create nice <script> tags to insert into the HTML.

const extractAssets = (assets, chunks) =>
Object.keys(assets)
.filter(asset => chunks.indexOf(asset.replace('.js', '')) > -1)
.map(k => assets[k]);
const extraChunks = extractAssets(manifest, modules).map(
c => `<script type="text/javascript" src="/${c.replace(/^\//, '')}"></script>`
);

Likewise, we need to tell Helmet to keep its head on a swivel — we got new tags to render!

const helmet = Helmet.renderStatic();

All of this is then thrown into a function that correctly injects all the right junk in all the right places:

const injectHTML = (data, { html, title, meta, body, scripts, state }) => {
data = data.replace('<html>', `<html ${html}>`);
data = data.replace(/<title>.*?<\/title>/g, title);
data = data.replace('</head>', `${meta}</head>`);
data = data.replace(
'<div id="root"></div>',
`<div id="root">${body}</div><script>window.__PRELOADED_STATE__ = ${state}</script>`
);
data = data.replace('</body>', scripts.join('') + '</body>');
  return data;
};

Everything is wrapped up into a nice little bow, run through the above function, and send off to the user:

const html = injectHTML(htmlData, {
html: helmet.htmlAttributes.toString(),
title: helmet.title.toString(),
meta: helmet.meta.toString(),
body: routeMarkup,
scripts: extraChunks,
state: JSON.stringify(store.getState()).replace(/</g, '\\u003c')
});
res.send(html);

We’re done! That’s about all there is to it. Let’s move on to how to implement a lot of the client-side functionality.

Back to React

Root render component: The whole purpose of this attempt is to make as few modifications to the React portion of the application. Of course, there have to be some global changes, but the core of your application shouldn’t need to change. The JSX of the application where we load on the client-side should now look like this:

// We're on the client side, note the lack of a req.url to create the store
const { store, history } = createStore();
const Application = (
<Provider store={store}>
<ConnectedRouter history={history}>
<Frontload noServerRender={true}>
<App />
</Frontload>
</ConnectedRouter>
</Provider>
);

The only differences you may notice are <ConnectedRouter> and <Frontload>. The first is the new replacement to <BrowserRouter> in React Connected React Router. The second is the higher-order component for React Frontload. In this case, we just explicitly define that we‘re not on the server yet via the noServerRender prop.

From here we move to render the Application accordingly:

const root = document.querySelector('#root');
if (root.hasChildNodes() === true) {
Loadable.preloadReady().then(() => {
hydrate(Application, root);
});
} else {
render(Application, root);
}

If we’re rendering in a production environment, we need to tell React Loadable to wait for the preload to be completed, before running hydrate. An important note: hydrate and render do the same thing. The only difference is that hydrate will presume the DOM is the same and only attach event listeners. This is incredibly helpful when server rendering and can save us time on the first load of the page. Oh my — how performant we are…

Store: The store is relatively straightforward as well. We’ve done everything we can to make this identical regardless of the environment (server or client). Here’s the entire store file:

import { createStore, applyMiddleware, compose } from 'redux';
import { connectRouter, routerMiddleware } from 'connected-react-router';
import thunk from 'redux-thunk';
import { createBrowserHistory, createMemoryHistory } from 'history';
import rootReducer from './modules';
// A nice helper to tell us if we're on the server
export const isServer = !(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
export default (url = '/') => {
// Create a history depending on the environment
const history = isServer
? createMemoryHistory({
initialEntries: [url]
})
: createBrowserHistory();
  const enhancers = [];
  // Dev tools are helpful
if (process.env.NODE_ENV === 'development' && !isServer) {
const devToolsExtension = window.devToolsExtension;
  if (typeof devToolsExtension === 'function') {
enhancers.push(devToolsExtension());
}
}
  const middleware = [thunk, routerMiddleware(history)];
const composedEnhancers = compose(
applyMiddleware(...middleware),
...enhancers
);
  // Do we have preloaded state available? Great, save it.
const initialState = !isServer ? window.__PRELOADED_STATE__ : {};
  // Delete it once we have it stored in a variable
if (!isServer) {
delete window.__PRELOADED_STATE__;
}
  // Create the store
const store = createStore(
connectRouter(history)(rootReducer),
initialState,
composedEnhancers
);
  return {
store,
history
};
};

Most of it is pretty well documented. You can read the comments above to know what’s going on.

The App: The main component of our application where we render everything out looks like such:

class App extends Component {
componentWillMount() {
if (!isServer) {
this.props.establishCurrentUser();
}
}
render() {
return (
<div id="app">
<Header
isAuthenticated={this.props.isAuthenticated}
current={this.props.location.pathname}
/>
<div id="content">
<Routes />
</div>
</div>
);
}
}

Fairly straight to the point again. We try to establish the state of the user (logged in or not). We also have a <Routes /> component which contains a <Switch> with all of our routes:

<Switch>
<Route exact path="/" component={Homepage} />
<Route exact path="/about" component={About} />
  <Route exact path="/profile/:id" component={Profile} />
  <AuthenticatedRoute exact path="/dashboard" component={Dashboard} />
  <UnauthenticatedRoute exact path="/login" component={Login} />
<AuthenticatedRoute exact path="/logout" component={Logout} />
  <Route component={NotFound} />
</Switch>

If you’re observant you’ll notice we have normal routes and two other kinds of routes: <AuthenticatedRoute> and <UnauthenticatedRoute>. These are pretty directly taken from the infamous Serverless Stack tutorial. I’ve modified them for my needs, but it’s essentially the same idea… we optionally show or don’t show a route depending on the state of the user. Simple enough!

Of course we have to import each of these routes. Traditionally this is done by a simple import Dashboard from './dashboard' or something of that nature. However, we can code split this for a little added sexiness.

const Dashboard = Loadable({
loader: () => import(/* webpackChunkName: "dashboard" */ './dashboard'),
loading: () => null,
modules: ['dashboard']
});

You’ll notice some weirdness in the webpackChunkName being specified in the loader property. This is currently a requirement since we can’t override the Webpack config of CRA without ejecting. I’d say that’s a worthwhile penalty. For more information on what’s going on here, check out this Github issue.

It’s also worth noting that it’s not necessarily perfect usage of the React Loadable library to split via Routes rather than Components. I should say that there’s no reason you can’t do both. For our purposes, I’ve just done Route splitting, but you are more than welcome to split on Components or do a combination of both.

Preloading data: By far the coolest part of this implementation is the ability to preload data before page load. Normally this would be done on the server by sniffing the page, determining what calls to dispatch, and then rendering the resulting state. However, I feel like this is a really bad separation of concerns. Instead we should leave page logic entirely within React where all the other dispatch calls belong anyhow.

This is entirely the point of React Loadable. Essentially what we’re doing is creating a catch-all higher-order component which looks at the currently displayed components and calls whatever data is needed for them. When running locally this places the call inside of componentDidMount — just like you would with any regular React app. When running on the server, all requests will be intercepted and the server response will be deferred until all Promises have completed. Super dope. Here’s how it works:

const frontload = async props =>
await props.getCurrentProfile(+props.match.params.id);
class Profile extends Component {
componentWillUnmount() {
this.props.removeCurrentProfile();
}
  shouldComponentUpdate(nextProps) {
if (nextProps.match.params.id !== this.props.match.params.id) {
this.props.getCurrentProfile(+nextProps.match.params.id);
}
    return true;
}
  render() {
const { name, id, image } = this.props.currentProfile;
    return (
<Page
id="profile"
title={name}
description={`This is user profile number ${id}`}
image={image}
>
<p>
<b>Name:</b> {name}
</p>
<p>
<b>ID:</b> {id}
</p>
<img src={image} alt={name} style={{ width: '400px' }} />
</Page>
);
}
}
const mapStateToProps = state => ({
currentProfile: state.profile.currentProfile
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ getCurrentProfile, removeCurrentProfile }, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(
frontloadConnect(frontload, {
onMount: true,
onUpdate: false
})(Profile)
);

As I mentioned, this call would normally be done in componentWillMount or componentDidMount:

componentDidUnmount() {
this.props.getCurrentProfile(+this.props.match.params.id);
}

With React Loadable, there are only two changes to make:

const frontload = async props =>
await props.getCurrentProfile(+props.match.params.id);

… and load the higher-order component…

export default connect(mapStateToProps, mapDispatchToProps)(
frontloadConnect(frontload, {
onMount: true,
onUpdate: false
})(Profile)
);

This species the request to load, frontload, and specifies when it should run. In my particular case, I don’t need it to run onUpdate because I handle that logic myself. If you want anymore information on how this works, Dave Williams posted the mother of all tutorials. Give it a read!

The Results

What else is there? We’ve pretty much covered everything. Let’s see how this works in action and call it a day!

Homepage

Our beautiful homepage with an actual picture of Jesus of Nazareth

About

What’s it all about?

Profile (first user)

All hail the Big Finn

Profile (second user)

All hail the Speedy Swede

An observant eye will notice the obvious throwback to my favorite Nashville Predators hockey players (who yet again had the Stanley Cup stolen from them two years in a row). This content is all page specific — it’s party time.

Wrapping Up

Of course, let me know if I got something wrong or if this could be improved or simplified. PR’s and issues are more than welcome! I hope to keep this somewhat updated with the community’s help. Here’s some helpful links:

Ironically, you won’t find me on any social media. So don’t bother. Feel free to follow me here and on Github if you like what you see!