Using Webpack's Hot Module Replacement with React

Out of the box, webpack supports a fancy new alternative to refreshing to see if your changes worked called hot module replacement, or HMR. At its core, HMR works by watching changes to files, and signaling the browser to replace specific modules, or functions, but not reload the entire page.

The major benefit of hot module replacement is that state is not lost – if you’ve had to perform a number of actions to get to a specific state, you won’t perform a full page refresh and lose the history of those actions.

Here it is at work – notice that the timer doesn’t reset to 0 as it would after a page reload, and css changes auto refresh too.

Hot Module Replacement at work in React
React Hot Module Replacement at work

Increasing the speed of your feedback loop frees time to do meaningful work and removes frustrating, repetitive activities from your workflow. If you’re interested in a more in depth explanation of how it works, check out the answer to What exactly is Hot Module Replacement in Webpack on Stackoverflow.

By the end of this, you’ll have webpack set up with React and HMR. If you get stuck at all, have a look at the example repo.

Acknowledgments: I have to give a big shout out and thank you to Dan Abramov, his open source work, and his talk Live React: Hot Reloading with Time Travel at react-europe. Without his contributions to the community, setting up HMR for React would be a lot more difficult. Thanks Dan!

Install and Configure Webpack

First, initialize a new project and install webpack.

mkdir react-hmr
cd react-hmr
git init
npm init
npm install -g webpack
npm install --save-dev webpack

Now, let’s get webpack set up. We’ll need three parts to make this work, some JavaScript, an index.html, and a webpack configuration to wire everything together.

The index.html will go in the base directory of the project.

<!DOCTYPE html>
<html>
  <head>
    <title>React HMR example</title>
  </head>
  <body>
    <div id="container"></div>
    <script src="/static/bundle.js"></script>
  </body>
</html>

Edit index.js to run a bit of JavaScript just so we know it’s working:

document.write("Webpack is doing its thing.");

Then add the following webpack configuration to webpack.config.js. Webpack requires an application entry point and an output file for the compiled JavaScript.

module.exports = {
  entry: "./index.js",
  output: {
    path: __dirname,
    filename: "bundle.js",
    publicPath: "/static/"
  }
}

Now, if you run webpack from the project directory, it will compile index.js to bundle.js. Great stuff, but it would be even more useful if we had a server running so that can view our results in the browser.

Webpack Dev Server

Webpack also has a light weight development server that we’ll be using to serve the assets that it compiles. We’ll use this going forward so that we can see the results of our work in the browser. Install it now:

npm install webpack-dev-server -g
npm install webpack-dev-server --save-dev

Now we can run webpack-dev-server and visit http://localhost:8080 to view the results of our work. πŸ‘

This is works, but we want our application to update when we make changes to it. Right now, if you alter the contents of index.js, nothing happens. Instead, we want webpack to recompile our application, and notify the page to reload. To do this, run the command with the following flags:

webpack-dev-server --progress --inline
  • --progress displays the compilation progress when building
  • --inline adds webpacks automatic refresh code inline with the compile application

TIP: Reduce global dependencies!

When sharing code with multiple people, it’s better to keep things simple. I recommend adding the following entry to package.json within the "scripts" object.

{
  "scripts": {
    "start": "webpack-dev-server --progress --inline"
  }
}

By referencing the webpack dev server within an npm script, rather than with a global executable we no longer rely on a global version of webpack and can use different versions of webpack in different projects. Now, when someone clones your repository, they can run npm install and npm start to launch the development server.

From now on, use npm start to run the webpack dev server.

Adding Babel and React

Now we’ve got a development environment that gives us quick feedback, so let’s add in the interesting stuff: Babel for transpiling JSX and ES2015 (and beyond!), and React for the UI.

As of Babel 6, we also need to install a few presets for ES2015 and JSX.

Shut down the dev server and install Babel:

npm install babel-core babel-loader babel-preset-es2015 babel-preset-react --save-dev

Now we need to add this to our webpack build pipeline. Add the following beneath the output object in webpack.config.js:

module.exports = {
  // entry and output options

  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel",
      include: __dirname,
      query: {
        presets: [ 'es2015', 'react' ]
      }
    }]
  }
}

Loaders are webpack’s equivalent to preprocessors. The webpack documentation describes them like this:

Loaders are kind of like β€œtasks” are in other build tools, and provide a powerful way to handle frontend build steps. Loaders can transform files from a different language like, CoffeeScript to JavaScript, or inline images as data URLs.

To test out the babel-loader and make sure it’s working with the presets, restart your dev server with npm start and change index.js to include some ES2015 flavoured JavaScript:

let docBody = "Webpack is doing its thing, with ES2015!";
document.write(docBody);

Webpack will compile this and the browser will update with the string we’ve used. Great! Now we can add React and ReactDOM into the mix by running:

npm install react react-dom --save

Note: Because React is required for our application to run, we’re using --save rather than --save-dev.

Now we’ll add a simple component and render it to the document body instead of the string we’re currently rendering.

We’ll create an App component in components/App.js:

import React, { Component } from "react";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
  }

  componentDidMount() {
    this.interval = setInterval(
      this.increment.bind(this),
      1000
    )
  }

  increment() {
    this.setState(({ counter }) => {
      return {counter: counter + 1};
    });
  }

  componentWillUnmount() {
    clearInterval(this.interval)
  }

  render() {
    const { counter } = this.state;

    return (
      <header>
        <div>Webpack is doing its thing with React and ES2015</div>
        <div>{counter}</div>
      </header>
    );
  }
}

Now, change index.js to import the component and render it to the container div:

import { render } from "react-dom";
import React from "react";
import App from "./components/App";

const containerEl = document.getElementById("container");

render(
  <App/>,
  containerEl
);

Now our component initializes a counter which increments a number every second. It’s not a very interesting component, but for our purposes it’s a good demonstration of how state is lost when the browser reloads a page. By default webpack will trigger an entire page reload, which means that we lose the component’s state. We can solve this with hot module replacement.

Enabling Hot Module Replacement (HMR)

To enable it for us, we need to turn on hot reloading within Webpack itself, and add the appropriate React tools to compile the modules with HMR support.

npm install --save-dev babel-preset-react-hmre

These are a Babel preset that allows HMR to be applied to React components. Because these are presets for Babel, the only thing we have to do to get it working is add react-hmre to our array of presets.

module.exports = {
  // entry and output options

  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel",
      include: __dirname,
      query: {
        presets: [ 'es2015', 'react', 'react-hmre' ]
      }
    }]
  }
}

and edit our start script in package.json to enable the hot option:

{
  "scripts": {
    "start": "webpack-dev-server --progress --inline --hot",
  }
}

Now everything should be working as we anticipated in our dev environment, but there are few final things to consider.

Caveats

The webpack config that we’ve put together here is very basic, if you bundle your app for production using this configuration you will be including HMR code within your app.

I would encourage you to modularize your webpack config for the different build environments you’ll need as production builds can differ greatly from what’s used in development.

That said, this is a great starting point for an initial webpack config!

If you get stuck, have a look at the example repo.

comments powered by Disqus