Hot Server Middleware

In the last episode we replaced our one server config with one for dev and one for prod and named them. We also create a bunch of new files.

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 hot-server
npm install

We've almost got SSR working in dev. Let's keep working:

In webpack.dev-client.js and webpack.prod-client.js let's take out the HTMLWebpackPlugin.

Comment out these lines:

// At the top:
// const HTMLWebpackPlugin = require("html-webpack-plugin")
...
// In the plugins array near bottom:
// new HTMLWebpackPlugin({
//   template: "./src/index.ejs",
//   inject: true,
//   title: "Link's Journal"
// })

When we npm run dev again, we see a new error in the browser. There's no more index.html, so webpack dev server needs a little help to serve the page.

Error on JSX

Hot Server Middleware

To the rescue comes a Deus Ex Package. The Webpack Hot Server Middleware. It's gonna give us SSR in development.

In terminal:

npm i webpack-hot-server-middleware

In express.js:

At the top:

const webpackHotServerMiddleware = require("webpack-hot-server-middleware")

In the if (isDev) { block:

server.use(webpackHotMiddleware) // Under this line
server.use(webpackHotServerMiddleware(compiler)) // Put this line

It takes both compilers as an argument.

Let's keep moving by running npm run dev again. We'll go through this error by error so you can see the common ones.

Error: illegal operation on a directory at MemoryFileSystem.readFileSync

Error: illegal operation on a directory at MemoryFileSystem.readFileSync is a crazy looking error. But what I've found is it's always pointing at your webpack-dev-server.js entry object.

entry: {
  server: ["./src/server/render.js"]
},

Should become:

entry: "./src/server/render.js",

Just a simple string with one entry point.

We've graduated past naming our bundles. We're going to let webpack handle that from now on.

Error: The 'server' compiler must export a function in the form of

This one is solveable by changing the libraryTarget option. Let's also change the final filename.

output: {
  filename: "dev-server-bundle.js",
  path: path.resolve(__dirname, "../build"),
  libraryTarget: "commonjs2"
},

So let's reload our server and see what we get.

Client/Server Markup Irregularities

Render Target Error

Hey, it works!

This is a good error to have. View source in your browser. We have real markup, in development! Whitespace is the culprit here. Let's update the render.js to get rid of this error. Remove the whitespace around the renderToString() output.

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

Hydrate

We have 2 new warnings. We're going to want to change what the client code does slightly. Inside app.js, change render to hydrate. This will save React some time, by only attaching events and not re-rendering everything that's already done.

Note: Your React and ReactDOM packages must be at least 16.0.0 for hydrate to work.

function render(Component) {
  ReactDOM.hydrate(
    <AppContainer>
      <Component />
    </AppContainer>,
    document.getElementById("react-root")
  )
}

Our last error is a weird one.

Image Error

In webpack.dev-server.js we need to add a forward slash to our name option.

{
  test: /\.jpg$/,
  use: [
    {
      loader: "file-loader",
      options: {
        name: "/images/[name].[ext]",
        emitFile: false
      }
    }
  ]
},

The Warning is because main.css isn't being output when webpack.dev-client.js runs. It still wants to use the style loader. Let's update it to be more like webpack.dev-server.js.

In webpack.dev-client.js:

// At the top

const ExtractTextPlugin = require("extract-text-webpack-plugin")
...

// Replace the css loader.
{
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: {
      loader: "css-loader"
    }
  })
},

...
// In the plugins
new ExtractTextPlugin("[name].css"),

Production

Okay, now that the development side is done, let's move on to production.

Let's see what happens when we run npm run prod.

Error app.use requires a middleware function

So in webpack.prod-server.js:

entry: "./src/server/render.js",

So now we need to switch the require back in express.js to point to our compiled output.

} else {
  webpack([configProdClient, configProdServer]).run((err, stats) => {
    const render = require("../../build/prod-server-bundle.js").default
    server.use(
      "/",
      expressStaticGzip("dist", {
        enableBrotli: true
      })
    )
    server.use(render())
    done()
  })
}

We've got the same errors for prod we had for dev. Let's fix this one.

WebpackOptionsValidationError: Invalid configuration object

We need to turn the functions in webpack.prod-client.js and webpack.prod-server.js into objects.

And the env.NODE_ENV into the string "production".

Let's keep going. What's next?

TypeError: render is not a function

So in webpack.prod-server.js:

output: {
  filename: "prod-server-bundle.js",
  path: path.resolve(__dirname, "../build"),
  libraryTarget: "commonjs2"  // Add this
},

Okay. Now, When we load this up in a browser, we can see we have SSR in production as well.

In Sum

We've come quite a long way. We're running the src/server/main.js file with both npm run dev and npm run prod, just using a different value for NODE_ENV. We're using a new package, webpackHotServerMiddleware to serve the entry point for the dev server, while using webpack.run() to hook the final compiled file, build/prod-server-bundle.js after the compilation as a piece of middleware. This is the real holy grail. The rest of this section is easy in comparison. We made a lot of changes and missing one is common. If you'd like to look at the final code for this article.

git checkout hot-server-final

Up Next

We're going to move ahead towards the ultimate setup by adding React Router to our client side. We'll hook into the server side and finally, we'll using dynamic imports to break our bundle into Router specific chunks.