Server Side Rendering with Express and React

In this section we're going to look at 2 features of Webpack, that until recently were nearly impossible to achieve at the same time. You could server render, or you could code-split, but doing both, well, no wonder so many people use Next.js. We'll start with an intro to Server-side rendering with Express and React, then we'll move into the world of dynamic importing and Universal Javascript. Where we use the same components on the server, that we ship to the client. Finally, we'll cap the section by delivering Asynchronous CSS and Javascript chunks as we navigate through our blog, using React Router. You're going to want to watch to the end of this one.

So first, we're going to look at rewiring our app for a real React style server-side render. Specifically it'll be an intro into hooking up the Server-side to match the Client-side javascript, and we'll expand our knowledge of SSR for the rest of the course in our effort to keep client/server parity as we add features.

We're going to start where we left off in the last article. If you need to catch up:

git clone https://github.com/lawwantsin/webpack-course.git
cd webpack-course
git checkout ssr-react
npm install

In express.js lets add a simple proof of concept.

import React from "react"
import ReactDOMServer from "react-dom/server"

Above the expressStaticGzip line add a simple Hello SSR! to render for JSX.

server.get("*", (req, res) => {
  const html = ReactDOMServer.renderToString(<div>Hello SSR!</div>)
  res.send(html)
})

This will use the renderToString function to turn the JSX into a plain string that express then sends in the response.

One more thing. We'll need to stop webpack from serving a plain index.html file. So in webpack.dev.js, let's comment out the following plugin:

// new HTMLWebpackPlugin({
//   template: "./src/index.ejs",
//   inject: true,
//   title: "Link's Journal"
// })

Okay, so now let's restart the server and check it out.

SSR Babel Error

Broken! It looks like Node, even Node 8 doesn't understand JSX. We'll need to use babel to get this working again.

npm install babel-preset-react

In .babelrc, add the plugin array under the presets array:

{
  "presets": [
    [
      "env",
      {
        "targets": {
          "browsers": ["last 2 versions"]
        },
        "debug": false
      }
    ]
    "babel-preset-react"  // Add this
  ]
}

Okay, when we run npm run dev and visit the site in a browser, we see a simple div without the html head & body stuff.

Hello SSR

Okay, that's a start. But what we'd like is to setup what we had before, with all the developer happiness and production optimization.

To do that we're going to need to separate how express works into development and production. Luckily, we're already doing that with the isProd variable. So let's add uncomment the aforementioned HTMLWebpackPlugin as that is a great way to develop and add a new way to render in production.

So in express.js under the Middleware enabled, we're going to put an else:

else {

  server.use(
    expressStaticGzip("dist", {
      enableBrotli: true
    })
  )

  server.use("*", (req, res) => {
    res.send(`
      <html>
        <head>
          <link href="/main.css" rel="stylesheet" />
        </head>
        <body>
          <div id="react-root">
            ${"Hello World"}
          </div>
          <script src='vendor-bundle.js'></script>
          <script src='main-bundle.js'></script>
        </body>
      </html>
    `)
   })
  }

Not Really

Almost. While babel can be employeed to transpile JSX, it can't just handle anything. Only JS. So as we build the rest of the page out to match what we used to have with index.ejs we run into problems.

In our express.js, let's add AppRoot.js in an attempt to load the full React page.

const renderToString = require("react-dom/server").renderToString

const AppRoot = require("../components/AppRoot").default

Then in the render function:

<div id="react-root">
  ${renderToString(<AppRoot />)}
</div>

And if we run npm run build and npm run prod, we've blown everything up. That's okay. We're nearly there.

At the top of AppRoot.js we can optionally require these assets if Webpack is doing the work, not express. This happens in both development and production when the main-bundle.js is built, so we need a new environment variable besides NODE_ENV.

In both the webpack.dev.js and webpack.prod.js, we will add the WEBPACK variable, so it'll be available only during bundle compilation.

new webpack.DefinePlugin({
  "process.env": {
    NODE_ENV: JSON.stringify(env.NODE_ENV),
    WEBPACK: true
  }
})
const MarkdownData = process.env.WEBPACK
  ? require("../../data/post.md")
  : { title: "Hello", __content: "World" }
const imagePath = process.env.WEBPACK
  ? require("../images/link.jpg")
  : "/images/link.jpg"

This code looks a bit hacky, and don't worry, it is.

The code now runs fine because it's bypassing the need to pull in non javascript files in express and only handling them with webpack. So when we run in development, everything still Hot-Reloads like you'd expect and in production. It doesn't error, but it also doesn't render the markdown and only uses a string for the image. (Not very dynamic).

This is to illustrate the difference webpack brings to the server-side. In order for our SSR to be useful to a web crawler, we'll need that markdown rendered as well. And we'll want to clean up that hacky requireing.

In Sum

In this article we took our first step toward Server side rendering with React and Express. Unfortunately we ran into a speed bump when trying to render other file types, namely markdown and images.

Solution? Use webpack on the server-side as well.

git checkout ssr-react-final

Next Up

We're going to create a new webpack config to compile the server code before reloading it in nodemon. In so doing, we'll leverage webpack even on the server-side and finally achieve the goal of Universal Javascript.