Hot Reloading with Webpack

I’ve created an awesome webpack configuration over the last few posts. I am handling both the ES2015 and SCSS stylesheet loads together with supporting CDNs for libraries and linting of the source files and stylesheets. However, there is still a tooling issue. I need to stop the server and rebuild every time I make a change to anything. I’d really rather this happen automatically so that the public files are built for me as soon as I save a file.

There happen to be two mechanisms of doing this – the “no-code” way and a mechanism that involves code. The idea is that I won’t be restarting anything at the end of this blog post – it will all happen for me.

The No Code Way of Rebuilding

My application is split into two parts – a server side built on top of ExpressJS and a client side that is loaded from the ExpressJS server. I need to deal with both of these elements. Building the public files automatically is easily done with Webpack and is a great first step. Here it is:

webpack --watch

I’ve got this established in my package.json file:

  "scripts": {
    "clean": "rimraf public",
    "pretest": "eslint server/src/**/*.js",
    "test": "mocha --require ignore-styles --recursive --compilers js:babel-register --reporter spec server/test",
    "watch": "webpack --watch",
    "prestart": "webpack -p",
    "start": "node ./bin/www"
  },

I open up two windows to use this mechanism. In the first window, I run npm start – this runs my server and builds the initial files. Once my server is listening for connections, I switch to my other window and run npm run watch. The Webpack process runs whenever I save a file in my editor. I can see the lint output straight away and change things. Then, when I’m ready to test my code, I can reload the browser and be testing straight away.

Of course, this doesn’t help me when I’m changing the server. I’m going to look at another utility for that functionality: nodemon. I can alter my watch command like this:

  "scripts": {
    "clean": "rimraf public",
    "pretest": "eslint server/src/**/*.js",
    "test": "mocha --require ignore-styles --recursive --compilers js:babel-register --reporter spec server/test",
    "watch": "webpack --watch",
    "nodemon": "nodemon --watch server --watch config --watch bin ./bin/www",
    "prestart": "webpack -p",
    "start": "node ./bin/www"
  },

In this case, nodemon is going to restart my server when anything used by the server changes – that includes three directories for me. That also means that the server will restart if I am changing the tests under server. However, I don’t expect to be doing live tests and mocha tests at the same time. I still need two windows. In the server window, I’m going to run npm run nodemon.

Something a little more complex…

The above mechanism is a good solution, but it does have its drawbacks. Firstly, I have to reload my page to see changes. I’m developing a single-page application and that means I need to restart the application when I’m checking stylesheets. I would much rather just the stylesheet be laoded. Secondly, it takes two windows. I’d rather the watching and reloading happen within the server process during development and then be served normally afterwards. This requires some code changes.

Firstly, let’s talk about NODE_ENV. This is an environment variable that you can use to decide what to do. I have configured my config directory to bring it into my configuration. My config/default.json now looks like this:

{
    "port": 3000,
    "env": "development",
    "auth": {
        "clientid": "NOT-SET",
        "secret": "NOT-SET",
        "domain": "NOT-SET"
    }
}

My config/custom-environment-variables.json is also changed to map NODE_ENV to the proper place:

{
    "port": "PORT",
    "env": "NODE_ENV",
    "auth": {
        "clientid": "AUTH0_CLIENTID",
        "secret": "AUTH0_CLIENTSECRET",
        "domain": "AUTH0_DOMAIN"
    }
}

On to the webpack.config.js. I needed to make some changes to the output and plugins sections:

    output: {
        path: path.join(__dirname, 'public'),
        publicPath: '/',
        filename: '[name].js'
    },
    plugins: [
        new ExtractTextPlugin('grumpywizards.css')
    ],

I’ve moved the place where the files are stored in the output.path property. This involves removing the same directory from the output.filename and the ExtractTextPlugin. In addition, I’ve defined the output.publicPath property – this is the location on the server where the files are going to be served.

Once you have done these changes, you should be able to run npm run prestart and the files will be generated in the same place as normal. Nothing has changed yet – just moving around some paths. Now for the magic.

In the server/src/app.js, I’m using the following call to set up /public as my static file area:

        app.use(staticFiles('public', {
            dotfile: 'ignore',
            etag: true,
            index: false,
            lastModified: true
        }));

The plan is to replace this with a special webpack middleware controller that will serve up the same files, but will reload them when they change. However, I only want this to happen in development. In production, I want the original static file serving to happen:

    if (config.env === 'development') {
        var compiler = webpack(webpackConfig);

        app.use(devServer(compiler, {
            publicPath: webpackConfig.output.publicPath || '/'
        }));
    } else {
        app.use(staticFiles('public', {
            dotfile: 'ignore',
            etag: true,
            index: false,
            lastModified: true
        }));
    }

Save that and run npm run nodemon. Once the log says the following:

Child extract-text-webpack-plugin:
    chunk    {0} extract-text-webpack-plugin-output-filename 5.38 kB [rendered]
        [0] ./~/css-loader?sourceMap!./~/postcss-loader!./~/sass-loader?sourceMap!./~/stylelint-loader!./client/src/components/Page.scss 3.87 kB {0} [built]
        [1] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]
webpack: bundle is now VALID.

You can load your web browser as soon as the bundle is valid.

I only need one window with this new configuration – the second window is gone and I have one command to run when I want to start the server. However, I’m still reloading the page to bring in the changes.

Hot Reloading

This is where it gets interesting. There are facilities within webpack to actually notify the browser when files have changed. This functionality is called Hot Module Reloading, or HMR for short. HMR requires changes to your webpack configuration and additional modules to work. This took a little time to master, so here is the recipe. Firstly, you need to make some changes to your webpack.config.js:

var webpack = require('webpack');

module.exports = {
    devtool: 'source-map',
    entry: [
        'webpack/hot/dev-server',
        'webpack-hot-middleware/client',
        path.join(__dirname, 'client/src/app.jsx')
    ],
    output: {
        path: path.join(__dirname, 'public'),
        publicPath: '/',
        filename: 'grumpywizards.js'
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new ExtractTextPlugin('grumpywizards.css')
    ],

I’ve added the definitions for the webpack hot middleware client to my entry. The entry becomes an array now. That means that the [name] tag in the output.filename property is meaningless, so I’ve replaced it with the full pathname of the output. Finally, I’ve added the HotModuleReplacementPlugin to the list of plugins.

Now, onto the server/src/app.js file:

        var compiler = webpack(webpackConfig);

        app.use(devServer(compiler, {
            publicPath: webpackConfig.output.publicPath || '/',
            stats: { colors: true }
        }));
        app.use(hotServer(compiler, {
            log: console.log
        }));

There is another piece of middleware that is explicitly the hotServer. This is provided by the npm package webpack-hot-middleware. Don’t try and use Winston in place of console.log here – it doesn’t work.

Once you have done this and restarted your server from scratch, you will note that there are some messages on the browser console that are interesting:

[HMR] Waiting for update signal from WDS...
[HMR] connected
client.js:106 [HMR] bundle rebuilding
client.js:108 [HMR] bundle rebuilt in 408ms
process-update.js:25 [HMR] Checking for updates on the server...
process-update.js:59 [HMR] The following modules couldn't be hot updated: (Full reload needed)
process-update.js:64 [HMR]  - ./client/src/components/Header.scss

This tells you that the hot module reloading is working, but the bundling is not. Not to worry – we can use that NODE_ENV environment variable to turn off the ExtractTextPlugin when the NODE_ENV is development. We can also turn off hot reloading when the NODE_ENV is not development. This is possible because the webpack.config.js is just another JavaScript file.

I’ve done some other optimizations around the code as well. For those that want to know:

  • I’ve separated out the index page and made the loading of the CSS optional based on the NODE_ENV
  • I’ve separated out the static pages into their own module
  • I’ve also adjusted the webpack.config.js to set up the right configuration based on NODE_ENV

Between all this, I am now hot-reloading all my files in development, but still serving up a combined CSS file in production. Check out the work in my GitHub Repository.