Skip to content

HMR guide rewrite #1439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 26, 2017
Merged
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 215 additions & 79 deletions content/guides/hot-module-replacement.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ contributors:
- joshsantos
- drpicox
- skipjack
- sbaidon
- gdi2290
related:
- title: Concepts - Hot Module Replacement
Expand All @@ -18,128 +19,220 @@ related:
url: /api/hot-module-replacement
---

T> This guide extends on code examples found in the [Development](/guides/development) guide.

Hot Module Replacement (or HMR) is one of the most useful features offered by webpack. It allows all kinds of modules to be updated at runtime without the need for a full refresh. This page focuses on __implementation__ while the [concepts page](/concepts/hot-module-replacement) gives more details on how it works and why it's useful.

W> __HMR__ is not intended for use in production, meaning it should only be used in development. See the [building for production guide](/guides/production) for more information.


## Enabling HMR

This feature is great for productivity. Let's take a look at how to set it up with [webpack-dev-server](https://github.com/webpack/webpack-dev-server)...

``` js
const path = require('path');
const webpack = require('webpack');
This feature is great for productivity. All we need to do is update our [webpack-dev-server](https://github.com/webpack/webpack-dev-server) configuration, and use webpack's built in HMR plugin.

module.exports = {
entry: './index.js',
__webpack.config.js__

plugins: [
new webpack.HotModuleReplacementPlugin() // Enable HMR
],
``` diff
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
+ const webpack = require('webpack');

module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
+ hot: true
},
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement'
}),
+ new webpack.HotModuleReplacementPlugin()
],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
```

output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
},
You can also use the CLI to modify the [webpack-dev-server](https://github.com/webpack/webpack-dev-server) configuration with the following command: `webpack-dev-server --hotOnly`.

devServer: {
hot: true, // Tell the dev-server we're using HMR
contentBase: path.resolve(__dirname, 'dist'),
publicPath: '/'
}
};
```
To get it up and running let's run `npm start` from the command line.

Not too bad, huh? Let's test it out using `module.hot.accept`...
Now let's update the `index.js` file so that when a change inside `print.js` is detected we tell webpack to accept the updated module.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to mention that the reader should npm start their webpack-dev-server before altering the print.js module.


__index.js__

``` js
import Library from './library';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this a diff and use the +and - for consistency :)

``` diff
import _ from 'lodash';
import printMe from './print.js';

if (module.hot) {
module.hot.accept('./library', function() {
console.log('Accepting the updated library module!');
Library.log();
})
}
+ if (module.hot) {
+ module.hot.accept('./print.js', function() {
+ console.log('Accepting the updated printMe module!');
+ printMe();
+ })
+ }

function component() {
var element = document.createElement('div');
var btn = document.createElement('button');

element.innerHTML = _.join(['Hello', 'webpack'], ' ');

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

element.appendChild(btn);

return element;
}

document.body.appendChild(component());
```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be able to just leave out this whole section as nothing has changed from the development guide.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did add the module.hot part, or do you mean doing something like this:

import _ from 'lodash';
import printMe from './print.js';

if (module.hot) {
    module.hot.accept('./print.js', function() {
    console.log('Accepting the updated printMe module!');
    printMe();
  })
}
...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No what you did was fine IMO, just make it a full diff ;)


__library.js__
Start changing the `console.log` statement in `print.js`, and you should see the following output in the browser console.

``` js
export default {
log() {
// Change this after the server is started to test
console.log('Initial log...')
__print.js__

``` diff
export default function printMe() {
- console.log('I get called from print.js!');
+ console.log('Updating print.js...')
}
}
```

Start changing the `console.log` statement in `library.js`, to `'Second log...'` for example, and you should see the following output in the browser console...
__console__

``` diff
[HMR] Waiting for update signal from WDS...
main.js:9998 Initial log...
main.js:9468 [WDS] Hot Module Replacement enabled.
+ 2main.js:9468 [WDS] App updated. Recompiling...
+ main.js:9468 [WDS] App hot update...
+ main.js:9912 [HMR] Checking for updates on the server...
+ main.js:9982 Accepting the updated library module!
+ 0.1bafc70….hot-update.js:11 Second log...
+ main.js:9955 [HMR] Updated modules:
+ main.js:9957 [HMR] - ./src/library.js
+ main.js:9894 [HMR] App is up to date.
main.js:4395 [WDS] Hot Module Replacement enabled.
+ 2main.js:4395 [WDS] App updated. Recompiling...
+ main.js:4395 [WDS] App hot update...
+ main.js:4330 [HMR] Checking for updates on the server...
+ main.js:10024 Accepting the updated printMe module!
+ 0.4b8ee77….hot-update.js:10 Updating print.js...
+ main.js:4330 [HMR] Updated modules:
+ main.js:4330 [HMR] - 20
+ main.js:4330 [HMR] Consider using the NamedModulesPlugin for module names.
```


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra line break is intentional -- we usually try to keep some spacing between sections (especially for h2s).

## Gotchas

Hot Module Replacement can be tricky. For example, let's say I have the following class:
Hot Module Replacement can be tricky. To show this, let's go back to our working example. If you go ahead and click the button on the example page, you will realize the console is printing the old `printMe` function.

This is happening because the button's `onclick` event handler is still bound to the original `printMe` function.

``` js
class Logger {
log(text) {
console.log('Logging some text: ', text)
To make this work with HMR we need to update that binding to the new `printMe` function using `module.hot.accept`:

__print.js__

``` diff
import _ from 'lodash';
import printMe from './print.js';

if (module.hot) {
module.hot.accept('./print.js', function() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is indented two tabs instead of one. However, I think this whole block should be changed to a diff and indented two extra lines.

console.log('Accepting the updated printMe module!');
- printMe();
+ document.body.removeChild(element);
+ element = component(); // Re-render the "component" to update the click handler
+ document.body.appendChild(element);
})
}
}
```

Even if the underlying module containing this class is patched with new code, any existing instances of the class still have the old `log` method. Meaning if we changed what that method does, it wouldn't be reflected in those old instances unless we re-instantiate them somehow using `module.hot.accept`.
let element = component();

function component() {
var element = document.createElement('div');
var btn = document.createElement('button');

element.innerHTML = _.join(['Hello', 'webpack'], ' ');

btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe; // onclick event is bind to the original printMe function

element.appendChild(btn);

return element;
}

document.body.appendChild(element);
```

This is just one example, but there are many others that can easily trip people up. Luckily, there are a lot of loaders out there, some mentioned below, that will make using this process much easier.
This is just one example, but there are many others that can easily trip people up. Luckily, there are a lot of loaders out there (some of which are mentioned below) that will make hot module replacement much easier.


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an extra line break between sections (similar to my other h2 comment above).

## HMR with Stylesheets

We can use the `style-loader` to achieve Hot Module Replacement with CSS. This loader uses `module.hot.accept` behind the scenes to patch `<style>` tags when CSS dependencies are updated. So, with the following webpack configuration...

``` js
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}
]
},
// ...
}
Hot Module Replacement with CSS is actually fairly straightforward with the help of the `style-loader`. This loader uses `module.hot.accept` behind the scenes to patch `<style>` tags when CSS dependencies are updated.

First let's install both loaders with the following command:

```bash
npm install --save-dev style-loader css-loader
```

hot loading stylesheets is a breeze...
Now let's update the configuration file to make use of the loader.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably add a quick installation line before the config changes, e.g.

npm install --save-dev style-loader css-loader

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also below the installation line you should put the name of the file that's being edited for consistency with the other guides e.g.

webpack.config.js

...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually thought about this, but I noticed that on the previous examples style-loader and css-loader are already added as dev-dependencies, should I write this just as a reminder in case they haven't been installed?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's only mentioned in asset-management which is then cleaned up at the end. I would mention it again just as a reminder.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it's also best to make sure that people who don't follow all the guides in succession are still able to fairly easily follow along with this particular guide.

I'd rather be a bit too verbose in these cases.


__webpack.config.js__

```diff
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
hot: true
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader']
+ }
+ ]
+ },
plugins: [
new HtmlWebpackPlugin({
title: 'Hot Module Replacement'
}),
new webpack.HotModuleReplacementPlugin()
],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
```

__index.js__
Hot loading stylesheets is as easy as importing them into a module:

``` js
import Lib from './library';
import './styles.css';
__project__

// ...
``` diff
webpack-demo
| - package.json
| - webpack.config.js
| - /dist
| - bundle.js
| - /src
| - index.js
| - print.js
+ | - styles.css
```

__styles.css__
Expand All @@ -150,8 +243,51 @@ body {
}
```

__index.js__

``` diff
import printMe from './print.js';
+ import './styles.css';

if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
document.body.removeChild(element);
element = component(); // Re-render the "component" to update the click handler
document.body.appendChild(element);
})
}

let element = component();

function component() {
var element = document.createElement('div');
var btn = document.createElement('button');

element.innerHTML = _.join(['Hello', 'webpack'], ' ');

btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe; // onclick event is bind to the original printMe function

element.appendChild(btn);

return element;
}

document.body.appendChild(element);
```

Change the style on `body` to `background: red;` and you should immediately see the page's background color change without a full refresh.
Copy link
Collaborator

@TheDutchCoder TheDutchCoder Jul 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would also include a diff here for the stylesheet instead of describing what to to.

E.g.

  body {
-   background: green;
+   background: red;
  }


__styles.css__

``` diff
body {
- background: blue;
+ background: red;
}
```


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing re line breaks.

## Other Code and Frameworks

Expand Down