Day 2, Segment 4, Part 2, Universal Rendering

In notebook:
FrontEndMasters React Intro 2
Created at:
2016-12-29
Updated:
2016-12-29
Tags:
JavaScript libraries React
22:30

Universal Rendering

So far the page source is empty. So you get nothing if you don't have JavaScript. The bigger risk is on slow connections, slow phones. 
We want to send down the complete markup for the page. Then while the user scans the page, the JavaScript will kick in and take over the rendering and especially handling the user interactions. 

Using NodeJS 

The app is of course written in JavaScript so you have to do the rendering in NodeJS. At Netflix the infrastructure is JVM based, so they have a NodeJS server that handles the requests and does the server-side rendering. The NodeJS server talks to the different services. 
This is what he recommends if your services are not running on NodeJS. 
(Also see Getify's MiddleEnd concept)

branch v2-23
26:30

App.js

We move most of what's in ClientApp into App.js. App.js will run in the client and the server but ClientApp will only run in the browser. 
ClientApp uses the ​render​ method which requires the DOM. 
  // ****   App.js    ****

import React from 'react'
import { BrowserRouter, Match } from 'react-router'
import { Provider } from 'react-redux'
import store from './store'
import Landing from './Landing'
import Search from './Search'
import Details from './Details'
import preload from '../public/data.json'

// this is just copied from ClientApp
// this a stateless functional component
// you don't need state here
const App = () => {
  return (
    <Provider store={store}>
      <div className='app'>
        <Match exactly pattern='/' component={Landing} />
        <Match
          pattern='/search'
          component={(props) => <Search shows={preload.shows} {...props} />}
        />
        <Match
          pattern='/details/:id'
          component={(props) => {
            const shows = preload.shows.filter((show) => props.params.id === show.imdbID)
            return <Details show={shows[0]} {...props} />
          }}
        />
      </div>
    </Provider>
  )
}

// instead of render, just export
export default App
Then ClientApp will be very simple, just render
  // ****   ClientApp.js    ****

import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter } from 'react-router'
import App from './App'

render(<BrowserRouter><App /></BrowserRouter>, document.getElementById('app'))
And finally update index.html.
We need to template our React string into this html (32:00). He uses Lodash template. You can use any method to insert your string there. 
  <!--****  index.html    ****-->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Frontend Masters React</title>
  <!--  Import CSS here, instead of in the JS bundle -->
  <!--  simpler to set up...  -->
  <link rel="stylesheet" href="/public/normalize.css">
  <link rel="stylesheet" href="/public/style.css">
</head>
<body>
  <div id="app"><%= body %></div>
  <script src="/public/bundle.js"></script>
</body>
</html>
Then we need to run Babel on the server. NodeJS doesn't speak JSX, ES6 import, all of ES6. 
Just like for test, we want Babel to transform the modules to commonjs. 
  // ****   .babelrc    ****

{
  "presets": [
    "react",
    ["es2015", {"modules": false, "loose": true}]
  ],
  "env": {
    "test": {
      "plugins": ["transform-es2015-modules-commonjs"]
    },
    // add server
    "server": {
      "plugins": ["transform-es2015-modules-commonjs"]
    }
  }
}
Write the server.js
  // ****   server.js   ****

// transpile the require statements on the server
require('babel-register')

// uses Express, because it's well known
// you can use Restify or HAPI as well
const express = require('express')
const React = require('react')
// it will render the app as string
const ReactDOMServer = require('react-dom/server')
const ReactRouter = require('react-router')
// pull out serverrouter
const ServerRouter = ReactRouter.ServerRouter
// lodash for templating
const _ = require('lodash')
const fs = require('fs')
const PORT = 5050
const baseTemplate = fs.readFileSync('./index.html')
// details are not important, it's just templating...
const template = _.template(baseTemplate)
// need to use .default to import from ES6 module
const App = require('./js/App').default

const server = express()

// static files serving
server.use('/public', express.static('./public'))

server.use((req, res) => {
  // see below for explanation (two pass rendering)
  const context = ReactRouter.createServerRenderContext()
  const body = ReactDOMServer.renderToString(
    // classic React rendering
    React.createElement(ServerRouter, {location: req.url, context: context},
      React.createElement(App)
    )
    // equivalent in JSX
    // <ServerRouter location={req.url} context={context}>
    //   <App />
    // </ServerRouter>
  )

// template it in the html and send it
// {body: body} ← take the body variable and 
// use it in the body placeholder 
// in the template (<%= body %>)
  res.write(template({body: body}))
  res.end()
})

// start listening
console.log('listening on port', PORT)
server.listen(PORT)
Don't use Babel-Node ever in production!!! Very slow. 

​ReactRouter.createServerRenderContext​ for server side rendering

Two-pass rendering
It uses a two pass system. It renders once, but if it has ​miss​ or ​redirect​, it will do a second pass. Otherwise just serve directly. 

Server side rendering is done now!

​$ NODE_ENV=server node server.js

We can even turn JavaScript completely off and still navigate our app (49:00)!!! 🏋️

Additional complexities

We have to be very careful not to use the DOM where it's not available (NodeJS). 

Checksum invalid errors in React

Can get issues when using Math.random()​ for example, as they are not going to be the same when rendered on the server and after the client. 
React gives a checksum invalid error and will re-render everything. You lose the benefit of server side rendering. The markup has to be the same on the server and the client. 

52:00

Q&A: Universal rendering and adding routes on the server

Notice that in server.js we did not have to add any routes. It reuses the ones we wrote for the browser. 

Other ways to achieve this routing

React Router can also accept a config file instead of the JSX method we used.  Then you could pass this config to the server. 

Q&A: Server side rendering and SEO?

Yes, it's good for SEO. This is one of the biggest advantage of ssr.