A step by step walkthrough of how SSR in React works

Or “Why SSR in React is painful.”

Not too long ago, I got suckered into building a site using React Serverside Rendering. The pitch was simple; the client needed an interactive site using 3d animations and stuff built with React. Unfortunately, a month in I learned that that only one page needed this level of interactivity and the rest of the page was typical CRUD stuff.

Having already built most of the site using react for client-side rendering, I figured I could render the client on the server and get the best of both worlds. While in the end I eventually got it working pretty well, it was a painful journey. This is the story of that journey.

What is SSR?

For those unfamiliar, Server Side Rendering or SSR is a technique in which your application code runs on the server and returns static HTML to the client. If you’re thinking, isn’t that how web apps have worked forever? Then you’re right. The “breakthrough” here though is with React (and a few other javascript libraries such as Ember 2.0 and Vue) you can build an application in one language (javascript) that works on both the client and the server.

Why is it useful?

Before client-side rendering became popular, developers would essentially build two versions of their app, one in a server-side language like PHP and another in javascript. The idea was that the PHP version would be the “basic” version of the app, and the client side version would be the “enhanced” version of the app. Take Gmail for example. Without javascript enabled, you can still view your email, but the experience is much better with javascript enabled. You have widgets and the ability to navigate around without reloading the page.

Eventually, developers got tired of trying to build two versions of their app, so they decided to use the server to send clients the javascript/client side version of the app. Doing so had a few adverse side effects:

  1. Interactivity suffered because the application is unusable until all the javascript is downloaded and executed. Writing large single page javascript apps used to mean that users were stuck with a loading page for seconds to minutes even when viewing static content

  2. Load times suffered because more content was being packed together, there was more content that needed to be loaded before the page could be viewed.

  3. SEO suffered, search engines like google don’t execute javascript, so essentially your client-side rendered site looks like a blank page to them.

In theory, using SSR, these issues could be resolved, by rendering the javascript on the server, your client instantly gets a working webpage, while developers only have to write one version of the app! Also, it’s easy to enable too; it can be as easy as

// You need some web server, express for example
import express from 'express'

// You need to load your app js
import client from './app/src'

// You need the handy react-dom/server
import ReactDOM from 'react-dom/server'

const app = express()

// And server your app to clients that request it
app.use('/app', () => (
  res.send(ReactDOM.renderToString(app))  
))


// And that's it, listen for requests!
app.listen(process.env.port || 3000)

Less than 20 lines of code and you can reap the benefits of SSR! Amazing right?

If only it were that simple

Of course, if it were that simple, I wouldn’t be writing this blog post. Depending on your version of nodejs, your babel configuration, whether the groundhog saw his shadow and whether you are generally a lucky person, you might stumble as soon as the second line of this example

import express from 'express'

because import isn’t supported natively in all versions of nodejs, and may be hidden behind a config flag.

Secondly, if you’re using JSX and or ES6 in your react app, you’ll likely find that you can’t directly import it without it being transpiled, so you’ll likely need to add webpack to this example to get it to load.

Assuming you get this to work, you’ll run into the next problems

Bundle splitting & Routing

The first time I got SSR working on this project, I was ecstatic! Although the config was a bit more complicated, it was pretty simple and seemed to work. Though I noticed a weird issue, when the page loaded, it took a few seconds before I could click anything or interact with the page.

Looking at the Chrome Devtools performance monitor, I quickly found the culprit; the javascript was taking over a second to parse!

It turned out, with my naive configuration, the bundle (singular at this point) was over 7 megabytes of uncompressed javascript. Essentially every module I used at some point in the site was being added to one big file that had to be downloaded and parsed before the site was usable.

So the first thing to do was to implement code splitting.

Code splitting is essentially telling webpack (or whatever module bundler you’re using) what code is used on what pages. When using the ES6 syntax this can be done semi-automatically. Compare the following:

Given the following files:

// login.jsx
import React from 'react'
import { reduxForms, Form } from 'redux-forms'

// ... other code

export default Login
// signup.jsx
import React from 'react'
import { reduxForms, Form } from 'redux-forms'

// ... other code

export default Signup
// admin.jsx
import React from 'react'
import { reduxForms, Form } from 'redux-forms'
import Users from 'users'
import Payments from 'payments'

// ... other code

export default Admin

Without code splitting:

bundle.js:
 - login.jsx
   - redux-forms.js
 - signup.jsx
   - redux-forms.js
 - admin.jsx
   - users.jsx
   - payments.jsx

with code splitting:

shared.js
 - redux-forms.js
 - react.js

login.js
 - login.jsx

signup.js
 - signup.jsx

admin.js
 - admin.jsx
 - users.jsx
 - payments.jsx

As you can see, code splitting will group shared modules into one bundle, then bundle modules based on how they’re used (commonly per page or module).

In my case, without code splitting, I was loading so much JS that the browser was choking on loading it, defeating one of the main benefits of using SSR! (interactivity)

At this point, I know I need code splitting, but how do I actually split the code?

Lesson one, React Router v4

React router is a library that essentially allows you to render different components based on which page you’re on, and if you look at the documentation, the way you’re essentially supposed to use it is like so:

import Login from './login'
import Signup from './signup'
import Admin from './admin'

const Comp = () => (
<App>
  <Switch>
    <Route path="/login">
      <Login/>
    </Route>
    <Route path="/signup">
      <Signup/>
    </Route>
    <Route path="/admin/:page">
      <Admin/>
    </Route>
  </Switch>
</App>
)

export default Comp

The keen observer may realize the problem with this immediately; if you import all your modules in one file, it won’t split the bundle facepalm

Lesson two, React Router v4 + React-loadable.

So as this wasn’t my first rodeo ride, I remembered from my last time setting up SSR, that you can have React Router call webpack’s require to dynamically load modules in, something like <Route path="/signup" component={() => require('./signup.tsx')} Unfortunately, this doesn’t directly work because react doesn’t allow async components (as of v16) however you can make this work using a dummy component + setstate but as not too lead you too far into the bushes, I used react-loadable which does roughly the same thing.

While this allowed components to be loaded ASYNCHRONOUSLY (great for not loading too much code upfront) it didn’t allow modules to be loaded synchronously.

Lesson three, an aside

I mentioned previously that there were two issues with the simple express based app serving scheme, and it’s related to why solutions #1 & #2 didn’t work. If you’ve been paying attention, you might wonder, how does the application know which route to render? On the client that’s easy, it merely looks at window.location but one recurring pain point of SSR is that you’re not running in the browser, so window definitely doesn’t exist. It turns out you need to inject the location into the app by using a different router implementation (in the case of React Router) like so

app.listen('/app', (req, res) => (
  const app = <StaticRouter location={req.location}>
    <App/>
  </StaticRouter>

  ReactDOM.renderToString(app)
))

// Side note, you can have JSX random in your server like this no problem, but you need to transpile it
// before using it in express/node. To achieve this, I have a separate file called render.js that gets compiled
// then required by index.js 

So with that fixed, it everything should work, right? Well…. of course not quite. So the best pattern I’ve found for making this work so far is not to define the routes inside the application directly, but inject them once the child components are known. We need a way to know which components match which routes ahead of time so that they can be synchronously loaded, that is, rendered in the “first pass” not once the page has already been loaded.

The react router team has a solution for this called react-router-config which essentially allows you to define your components in an object, then use methods like matchRoute to give you the component(s) for the current route. After some refactoring I got this to work, but as soon as it was done, I faced the next sync/async challenge

Loading Data

To understand why loading data is a challenge, let’s walk through the flow thus far:

  1. The user visits /admin/users
  2. React router mounts the <Admin> component inside the <App> component
  3. Because we’re using React-router-config, it knows the <UserList> component is a child of <Admin> and mounts it too.
  4. <UserList>’s ComponentWillMount() fires an async ajax call to load the users
  5. The page renders to the client
  6. <UserList>s ComponentWillMount() completes and populates the data to the store on the server.
  7. The user sees a perpetually empty user list.

Of course, this is not the desired behavior. We want to make our async calls, then only render when all the calls have completed. To get this to work took a bit of finagling as well. Let’s decompose the problem

Q: How do we know when the data has finished loading? A: We can use promises

Q: How do we get promises for loading data? A: We can have a loadData method that returns a promise

Q: How do we know what components need to load data? A: …. hm. Create a convention where having a loadData method == I need to load data?

So that’s what I did, for each function that needed to load some data before it rendered I added a static loadData(params): Promise<any> to them and added a little method like

loadDataBeforeRender(comps): Promise<any>
   let promises: Promise<any>[] = [];

  // pseudo code
  // for each comp
  //   check if it has a loadData() method
  //    if so, call it, and add it's returned value to promises
  return Promise.all(promises)

so now my render method looked like

import routes from './app/src/routes'

(req, res) => {
  const comps = matchRoutes(routes, req.location)
  app = <StaticRouter location={req.location}>
    <App>
      { comps }
    </App>
  </StaticRouter>

  loadDataBeforeRender(comps).then(() => ReactDOM.renderToString(app), () => res.send(500));
}

But wait, where does the data go?

This would probably work if all your components are ComponentClasses instead of StatelessComponenets but being the hipster I am, I, of course, went for stateless components.

As a refresher, recall that StatelessComponents are Pure functions, so they take “props” and return Dom elements. ComponentClasses have state (updatable by calling setState and will call render() as the component deems necessary.

Since all my components are stateless, the props need to live somewhere between the data being fetched and the component rendering. This was taken care of by adding Redux and Redux Thunk to the mix. Redux allows me to dispatch an action and then components can subscribe to the redux store for an update to their props. Redux thunk allows me to get a promise for the dispatched action so I can implement something like

# user_reducer.js

{
  [LOAD_USER_STARTED]: (state, action) => {
    return { loading: true, error: null, users: [] }
  },
  [LOAD_USER_SUCCESS]: (state, action) => {
   return { loading: false, error: null, users: action.payload.users }
  }
  // ... etc
}


# user.jsx

static loadData(dispatch) {
  dispatch('LOAD_USER')
}

const Users = ({users}) => (
 <ul>
  { users.map(user => ( <li key={user.id}>{user.name}</li>)) }
 </ul>
 )
  
// Simplified this example since I used recompose to hoist the static, and to bind dispatch
 export default connect((state) => {users: state.users})(Users)

With this setup, when the promise(s) resolve the redux store would be filled with data, and thus on “first render” the components would render synchronously with their data, perfect.

Not all that glitters is gold

With this, I can now curl the backend with curl and get a HTML page with the expected users’ list, great! Hmm, it seemed pretty slow though. Looking at the server console, it took almost 2 seconds to render this simple page! Also, when I load the page on the client, it re-renders a few times before being usable. What’s going on? Well, this post has been going on for a while so I’ll talk about the difficulties of using SSR on the frontend in the next post. :