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.
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.
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.