#7 Step By Step: Hot Module Replacement
Friday, August 24, 2018
In this article we will take a step by step approach to setting up hot module replacement for both our javascript and css styles for our index bundle. In order to do this we will also implement a very simple server using express js.
Install some middleware
- WebUi
- package.json
- webpack.config.js
The first thing we are going to do is to install a package from npm. I know the shock of it all. The package that we are going to install is a piece middleware that we will need to enable hot module replacement for our bundles (a). The versions of the package at the time of the writing of this article is shown in (b).
Terminal
npm install --save-dev webpack-hot-middleware
package.json
{
...
"devDependencies": {
...
"webpack-hot-middleware": "^2.22.2"
}
}
Now that we have installed the package we are going to make use of it. If we take a look at the documentation for the middleware, which you take a look at on GitHub, we will see that it lets us know that we need to include the client side code with the bundles generated for each of our entry points. The way that we do this is by adding an additional string to the array for our entry points as shown in (c).
webpack.config.js
...
const entry = {
"index": ["./Source/index.ts", "./Source/index.scss"]
}
entry["index"].push("webpack-hot-middleware/client");
...
Once we have everything up and running we will be a little more robust in the way that we add the string to our entry points. Once that is done and we rebuild our bundle and refresh our browser we should see the error shown in (d).

No server no connection
- WebUi
- package.json
Sticking with our hands approach that we have been using so far we are going to take care of creating a simple little server using express which is a web framework for node applications. And while we are installing it from npm we will also grab another middleware package that we need (e). As usual the versions for the packages at the time of the writing of this article are shown in (f). Also make sure that you add the 'express:dev' key to the scripts section.
Terminal
npm install --save-dev
express
webpack-dev-middleware
package.json
{
...
"devDependencies": {
...
"express": "^4.16.3",
...,
"webpack-dev-middleware": "^3.1.3",
...
},
"scripts": {
...
"express:dev": "node server.dev.js",
...
},
...
}
Get that server up and running
- WebUi
- server.dev.js
In terms of setting up our server we are going to do the bare minimum that we need to do in order to get everything up and running which is shown in (g). I think most of the code is self explanatory. We are simply going to respond to a single route, which is the root route, on localhost port 8000 by serving our static index.html file. We are passing a bit of configuration to both of our middlewares. You can of course check out the documentation for all of the options for the webpack dev middleware and the webpack hot middleware at any point.
server.dev.js
const express = require("express");
const LOCAL_HOST_PORT = 8000;
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");
const webpackHotMiddleware = require("webpack-hot-middleware");
const config = require("./webpack.config");
const compiler = webpack(config);
const app = express();
app.use(express.static(__dirname));
app.use(webpackDevMiddleware(compiler,
{
publicPath: config.output.publicPath,
stats: { colors: true }
}));
app.use(webpackHotMiddleware(compiler,
{
log: console.log
}));
const router = express.Router();
router.get("/", (_, res) => res.render("index"));
app.use(router);
app.listen(LOCAL_HOST_PORT, () => console.log(`listening on ${LOCAL_HOST_PORT}`));
With all of that done we can once again recreate our bundles just to make sure that everything is
still working as it should be. Once that is done we can start our server by invoking the
npm run express:dev
script that we added previously.
If we go ahead and refresh our browser again we should see
that we are receiving an error telling us that hot module replacement, HMR, is disabled
(h).

On to the next step
- WebUi
- webpack.config.js
We are receiving the error shown in (h) because we have not included the hot module replacement plugin in our webpack configuration file. This is a very easy thing to fix and the solutions is shown in (i).
webpack.config.js
...
const plugins = [
...
]
plugins.push(new webpack.HotModuleReplacementPlugin());
...
I have chosen to add the plugin using the push method of the array instead of simply including it in the initialization since we will only want to include it in our development bundles but we will take care of that a little latter on. Once again we need to rebuild our bundles and refresh the browser. Once we do that we should see that the last error is gone and the new message is telling us that HMR is connected (j).

Not so fast
- WebUi
- Source
- index.ts
- Source
Our browser console seems to be telling us that everything is now working and that means it is time to test it out. To do this we are simply going to return to our index typescript file and change the text in our console log statement by adding a single exclamation point to the end (k).
index.ts
console.log("hello world from typescript!");
Once we have done that and save the changes we will be greeted by the warning shown in (l) telling us that the changes that we just made have been ignored.

Stop ignoring me
- WebUi
- package.json
The reason our changes are being ignored is that hot module replacement is an opt in feature and we have not opted in with our index bundle. To opt in the first thing we are going to do is install the type definitions for the webpack environment using the command shown in (m). Of course by now you know the drill, the version for the package is shown in (n).
Terminal
npm install --save-dev @types/webpack-env
package.json
{
...
"devDependencies": {
"@types/webpack-env": "^1.13.6",
...
},
...
}
Opt me in
- WebUi
- Source
- index.ts
- Source
In order to opt in for HMR all we need to do is add the statement to our entry point files shown in (o).
index.ts
if (typeof module.hot !== "undefined") {
module.hot.accept();
}
console.log("hello world from typescript!");
Once we have opted in any changes that we make to our index javascript bundle will be sent to the browser and updated without us needing to refresh anymore. At some point in time though we will think to ourselves we need to check and make sure it is working for our scss files as well. Unfortunately we we make a change to our styles we will see that they are not being updated as we had hoped.
What about my styles?
- WebUi
- package.json
- webpack.config.js
Fortunately for us the fix to make everything work with our stylesheets as well is very simple. We just have to remember that when we got everything up and running that we included a plugin that removed our css from the javascript bundles and placed them in their own bundle which is being saved to the disk. What we need is to have those styles dynamically injected into our page the way our javascript updates are being injected. To do this we need to install another webpack loader. This time it is the style loader (p). The version of the style loader at the time of the writing of this article is shown in (q).
Terminal
npm install --save-dev style-loader
package.json
...
{
...
"style-loader": "^0.21.0",
...
}
}
What we want is when we are developing for our styles to be served in memory and when we are in production for them to be served from the disk. To do this we just need to modify our scss rule slightly (r).
webpack.config.js
...
const isDev = mode === "development";
...
const _module = {
rules: [
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
isDev ? "style-loader" : MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"sass-loader"
]
},
...
]
}
...
With this last change made everything should be up and running for both our javascript and our css. To make things a little easier to follow you can see the complete code, as they stand right now, for our webpack.config.js file (s) and our server.dev.js file (t) shown below. In the webpack configuration I have made a change to how the client side HMR code is added to our entry points as well as when the hot module replacement plugin is used.
webpack.config.js
const mode = process.env.NODE_ENV || "development";
const isDev = mode === "development";
const webpack = require("webpack");
const autoprefixer = require("autoprefixer");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const entry = {
"index": ["./Source/index.ts", "./Source/index.scss"]
}
if (isDev) {
Object.keys(entry).forEach(x => {
if (typeof entry[x] === "string") {
entry[x] = [entry[x]];
}
entry[x].push("webpack-hot-middleware/client");
});
}
const _module = {
rules: [
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
isDev ? "style-loader" : MiniCssExtractPlugin.loader,
"style-loader",
"css-loader",
"postcss-loader",
"sass-loader"
]
},
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
"ts-loader"
]
}
]
}
const output = {
filename: "[name].bundle.js",
path: __dirname + "/wwwroot/js/"
}
const plugins = [
new MiniCssExtractPlugin({
filename: "../css/[name].bundle.css"
}),
new webpack.LoaderOptionsPlugin({
options: {
postcss: [
autoprefixer()
]
}
})
]
if (isDev) {
plugins.push(new webpack.HotModuleReplacementPlugin());
}
module.exports = {
entry,
mode,
module: _module,
output,
plugins
}
server.dev.js
const express = require("express");
const LOCAL_HOST_PORT = 8000;
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");
const webpackHotMiddleware = require("webpack-hot-middleware");
const config = require("./webpack.config");
const compiler = webpack(config);
app = express();
app.use(express.static(__dirname));
app.use(webpackDevMiddleware(compiler,
{
publicPath: config.output.publicPath,
stats: { colors: true }
}));
app.use(webpackHotMiddleware(compiler,
{
log: console.log
}));
router = express.Router();
router.get("/", (_, res) => res.render("index"));
app.use(router);
app.listen(LOCAL_HOST_PORT, () => console.log(`listening on ${LOCAL_HOST_PORT}`));