John Stewart

Hi! I'm John. I'm a developer, coder, geek, tech head, maker of websites at MLB. JavaScript is what I do. React and Redux fan.

Webpack HMR with Express App

it's hot

Recently, my team explored using a framework to build a new site, and decided to use React. As the main developer, I wanted to make sure I explored everything that React and Webpack have to offer for creating the best development experience.

I specifically wanted Webpack's Hot Module Replacement(HMR). I had seen demos and played around with it a bit, but had zero experience integrating HMR into an existing Express app.

This post walks through the steps to get Webpack HMR working in an Express app, plus some opinions on optimizing for maintenance and cleanliness.

Let's get into it!

Install Dependencies

First, install the dependencies needed to get this working:

Open up a terminal and run the following line:

npm install webpack webpack-dev-middleware webpack-hot-middleware -save-dev

Setup Middlewares

Next, to integrate the middlewares into an existing Express app. This tutorial uses a simple example, but the most critical part is having access to the actual Express application instance. Take a look at an example app.

index.js

const express = require("express");
const app = express();
const path = require("path");

// static assets
app.use(express.static("public"));

// main route
app.get("/", (req, res) =>
  res.sendFile(path.resolve(__dirname, "./public/index.html"))
);

// app start up
app.listen(3000, () => console.log("App listening on port 3000!"));

Now, attach the middlewares to the Express app.

index.js

const express = require("express");
const app = express();
const path = require("path");
const webpack = require("webpack");

const webpackConfig = require("./webpack.config");
const compiler = webpack(webpackConfig);

// webpack hmr
app.use(
  require("webpack-dev-middleware")(compiler, {
    noInfo: true,
    publicPath: webpackConfig.output.publicPath
  })
);

app.use(require("webpack-hot-middleware")(compiler));

// static assets
app.use(express.static("public"));

// main route
app.get("/", (req, res) =>
  res.sendFile(path.resolve(__dirname, "./public/index.html"))
);

// app start up
app.listen(3000, () => console.log("App listening on port 3000!"));

For HMR to work, use the webpack-dev-middleware and webpack-hot-middleware modules. These will allow the Express app to track changes, and push those changes to the client code.

The middlewares require Webpack and a Webpack config. They listen to the client side code changes and then communicate to each other through the __webpack_hmr route.

This is all the integration on the Express-side.

Set Up Webpack to use HMR

Now that we have the basics in our Express app, set up the Webpack config:

const webpack = require("webpack");
const path = require("path");

module.exports = {
  entry: {
    index: [
      "webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000",
      "./src/index.js"
    ]
  },
  output: {
    path: path.resolve(__dirname, "./public"),
    filename: "[name].bundle.js",
    publicPath: "/"
  },
  plugins: [new webpack.HotModuleReplacementPlugin()]
};

Above is a simple Webpack config. The first key part is the extra entry for the main client-side code. This additional entry communicates with the Express app to receive new code updates without having to refresh the page.

The second key part is adding the webpack.HotModuleReplacementPlugin to the Webpack plugins property, which enables HMR.

That is all we need to do to update the Webpack config to work with HMR.

Set Up Client Code to use HMR

Now that the app uses HMR, it's time to enable it in the client-side code.

Depending on if you are using React or Vue or something else, each community might already support HMR integration. Here are some that provide HMR support:

These libraries provide HMR support, making hook up even more straightforward.

This example comes straight from the Webpack docs and uses plain old Vanilla JavaScript.

index.js

import printMe from "./print.js";

function component() {
  const element = document.createElement("div");
  const btn = document.createElement("button");

  btn.innerHTML = "Click me and check the console!";
  btn.onclick = printMe;

  element.appendChild(btn);

  return element;
}

let element = component();
document.body.appendChild(element);

if (module.hot) {
  module.hot.accept("./print.js", function() {
    document.body.removeChild(element);
    element = component();
    document.body.appendChild(element);
  });
}

print.js

export default function printMe() {
  console.log("Hi there!");
}

Here we have two files. The first, our main index.js file sets up the root of our app. Next, a print.js file exports a default function for use within the - index.js file.

Within the root of the app, a component() function creates a button element and binds the click event to the printMe() function (exported from print.js). Then, the component() function creates an element to mount to the body of the page.

At the bottom of the index.js file, we set up HMR. First, check if we have any hot modules, then accept changes from the print.js file. Once anything changes in the print.js file, assume that the bundle has been patched with the updated code.

Next, remove the original element, since it contains a stale reference the printMe() function. Once removed, create a new button with the updated reference and append it to the body.

Run It

Everything should be setup and good to go. Run the Express app and the client side bundle* however you usually would.

You should now see the standard Webpack stats output within the logs of your Express app and in your client-side script output.

example output webpack output

I don't like the idea of polluting my Express app's logs (even though it's harmless) but I'll leave that up for you to decide. Or read below for a solution to it!

*- I'm not entirely sure if you have to run the client build, but I run both currently and things are working well. If anyone knows if this is necessary or any negatives to doing so please share.

Testing HMR

Time to test the code by loading it up and seeing the HMR in action.

The console should show something like this:

[HMR] connected

That's a good sign! Test the button by clicking, which should yield an output like:

[HMR] connected
Hi there!

Moment of truth: update what's inside print.js. Add some new text to log to the console, and click the button again. The console should show the following output:

[HMR] connected
Hi there!
[HMR] bundle rebuilding
[HMR] bundle rebuilt in 130ms
[HMR] Checking for updates on the server...
[HMR] Updated modules:
[HMR]  - ./src/print.js
[HMR] App is up to date.
Updated hello!

Yes!

Clean up HMR output files

You may have noticed that the output directory is filling up with a bunch of files that look like this:

hmr files

This is a problem, and something to avoid. Get around this by updating the Webpack config a bit:

const webpack = require("webpack");
const path = require("path");

module.exports = {
  devtool: "eval-source-map",
  entry: {
    index: [
      "webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000",
      "./src/index.js"
    ]
  },
  output: {
    path: path.resolve(__dirname, "./public"),
    filename: "[name].bundle.js",
    publicPath: "/",
    hotUpdateChunkFilename: ".hot/[id].[hash].hot-update.js",
    hotUpdateMainFilename: ".hot/[hash].hot-update.json"
  },
  plugins: [new webpack.HotModuleReplacementPlugin()]
};

Add two new properties to the Webpack config: hotUpdateChunkFilename and hotUpdateMainFilename. These properties update the name of the generated files and prepend it with a directory. I used the directory .hot. Take this a step further, update .gitignore to ignore anything within that directory:

./public/.hot/*

When the app runs HMR output changes should look like:

updated hmr files

Clean Up Logging

As mentioned above, pass in stats: false as an option for the webpack-dev-middleware to clean up Webpack stats output from the Express log.

app.use(
  require("webpack-dev-middleware")(compiler, {
    noInfo: true,
    publicPath: webpackConfig.output.publicPath,
    stats: false
  })
);

Thoughts

After using this setup with HMR for about a week, everything is going well.

Occasionally, I'll notice that HMR doesn't update correctly. The module is nice enough to alert you when it might not get things and requires a full page refresh. For the most part, when I'm updating styles or modifying React components, it updates immediately.

If you have the opportunity to help improve your development team's workflow, I recommend giving HMR a shot.

If you liked this article and want to say hi, then you can find me on Twitter.