React Hot Loading with Webpack 2

A brief guide to implementing hot loading with React components

Posted by John Glynn on August 15, 2017

React components, hot loaded? Using the Webpack Hot Module Replacement (HMR) plugin with webpack-dev-middleware, webpack-hot-middleware and react-hot-loader, you can get hot loading of React components the same as regular Javascript modules.

What is hot loading and why would you want it? Hot loading is the replacement of code in a running application without requiring a restart or reloading of the application. It is very useful when developing because you can see your changes as you make them thus giving immediate feedback. React hot loading depends on a working Webpack HMR setup so we’ll start with that.

First, we’ll start with a minimal Webpack configuration.
A single entry

    entry: [
        './public/scripts/main',
    ]

and a Webpack 2 style module with a rule for Babel

    module: {
        rules: [
            {
                test: /\.json$/,
                use: 'json-loader'
            },
            {
                test: /\.jsx?$/,
                use: [{loader: 'babel-loader'}],
                exclude: [
                    path.resolve(__dirname, 'node_modules')
                ]
            }
        ]
    }

Using a common rule for the loader simplifies the Webpack config and environment differences can be broken out in the .babelrc file. For example,

{
    "env": {
        "test": {
            "presets": ["react", "es2015", "stage-2"],
            "plugins": [
                ["istanbul", {"exclude": ["test"]}]
            ]
        },
        "prod": {
            "presets": ["react", "es2015", "stage-2"]
        }
    }
}

Each specialization is accessed by setting BABEL_ENV in the shell environment.

Adding HMR is easy. Create a plugins array or extend yours with the HMR plugin.

      plugins: [
        new webpack.HotModuleReplacementPlugin()
      ]

To actually use HMR requires a bit more work. Webpack-dev-middleware needs to be setup and another package, webpack-hot-middleware, will need to be used to handle the hot loading plugin for the middleware. The following steps are needed to configure express; usually in ./bin/www in your project or a file required from there:

// HMR configuration
if (process.env.NODE_ENV !== 'production') {
  const webpack = require('webpack')
  const webpackDevMiddleware = require('webpack-dev-middleware')
  const webpackConfig = require('./webpack.dev.config')
  const middlewareOptions = {
    stats: { colors: true },
    noInfo: false,
    lazy: false,
    headers: {
      "Access-Control-Allow-Origin": "http://localhost"
    },
    publicPath: webpackConfig.output.publicPath
  }
  const compiler = webpack(webpackConfig);
  const webpackDevMiddlewareInstance = webpackDevMiddleware(compiler, middlewareOptions);
  app.use(webpackDevMiddlewareInstance)

  const webpackHotMiddleware = require('webpack-hot-middleware')
  app.use(webpackHotMiddleware(compiler))
}

Lastly, you will need to add another entry point for the hot loader client

      entry: [
          'webpack-hot-middleware/client',
          './public/scripts/main',
      ]
  • If you use a proxy, you will also need to be aware of the HMR callback at localhost/__webpack_hmr. This is the purpose of the Access-Control-Allow-Origin header specified in the middleware options.

At this point, we have HMR working. A change in the source will cause a partial rebuild of the Webpack bundle into a new small file with the changed code, a mapping file and the original bundle. This is why the entry point for the hot loader client was added to the array on top of the original entry point. For example,

Original build:

webpack built 2582a08558ae8c6cb3bf in 24655ms
Hash: 2582a08558ae8c6cb3bf
Version: webpack 2.6.1
Time: 24655ms
    Asset     Size  Chunks                    Chunk Names
bundle.js  26.2 MB       0  [emitted]  [big]  main
chunk    {0} bundle.js (main) 9.29 MB [entry] [rendered]
    [0] ./~/react/react.js 59 bytes {0} [built]
   [14] ./~/react-dom/index.js 59 bytes {0} [built]
  [910] ./public/scripts/main.jsx 2.39 kB {0} [built]
  [912] (webpack)-hot-middleware/client.js 6.68 kB {0} [built]
 [1077] ./public/scripts/appNavbar.js 755 bytes {0} [built]
 [1078] ./public/scripts/commonLib.js 547 bytes {0} [built]
 [1111] ./public/scripts/i18n.js 2 kB {0} [built]
 [1120] ./public/scripts/navbarHeaderSettings.js 1.24 kB {0} [built]
 [1160] ./public/scripts/store/main.js 1.08 kB {0} [built]
 [1323] ./~/history/es/index.js 460 bytes {0} [built]
 [1776] ./~/react-hot-loader/index.js 41 bytes {0} [built]
 [1782] ./~/react-hot-loader/lib/patch.js 209 bytes {0} [built]
 [1978] ./~/strip-ansi/index.js 161 bytes {0} [built]
 [2016] multi webpack-hot-middleware/client ./public/scripts/main 52 bytes {0} [built]
     + 2002 hidden modules
webpack: Compiled successfully.

After a modification:

webpack: Compiling...
webpack building...
webpack built 3ca752230fba1bc44ed9 in 6329ms
Hash: 3ca752230fba1bc44ed9
Version: webpack 2.6.1
Time: 6329ms
                               Asset      Size  Chunks                    Chunk Names
                           bundle.js   26.2 MB       0  [emitted]  [big]  main
0.2582a08558ae8c6cb3bf.hot-update.js   71.5 kB       0  [emitted]         main
2582a08558ae8c6cb3bf.hot-update.json  43 bytes          [emitted]         
chunk    {0} bundle.js, 0.2582a08558ae8c6cb3bf.hot-update.js (main) 9.29 MB [entry] [rendered]
    [0] ./~/react/react.js 59 bytes {0}
   [14] ./~/react-dom/index.js 59 bytes {0}
  [910] ./public/scripts/main.jsx 2.39 kB {0} [built]
  [912] (webpack)-hot-middleware/client.js 6.68 kB {0}
 [1077] ./public/scripts/appNavbar.js 755 bytes {0} [built]
 [1078] ./public/scripts/commonLib.js 547 bytes {0} [built]
 [1111] ./public/scripts/i18n.js 2 kB {0} [built]
 [1120] ./public/scripts/navbarHeaderSettings.js 1.24 kB {0} [built]
 [1160] ./public/scripts/store/main.js 1.08 kB {0} [built]
 [1323] ./~/history/es/index.js 460 bytes {0}
 [1776] ./~/react-hot-loader/index.js 41 bytes {0}
 [1782] ./~/react-hot-loader/lib/patch.js 209 bytes {0}
 [1978] ./~/strip-ansi/index.js 161 bytes {0}
 [2016] multi webpack-hot-middleware/client ./public/scripts/main 52 bytes {0}
     + 2002 hidden modules
webpack: Compiled successfully.

But React at this point is not hot loading yet. A manual reload is still needed. The approach taken with react-hot-loader is much the same as the one taken by HMR with webpack-hot-middleware; a wrapper with its own entry point.

  • The React examples from here on use React Router v4. It is highly recommended to use v4 if you are using routing since they are real React components and will hot load properly.
  • I will ignore import statements and propType definitions for brevity.

Here is our example application’s main component:

export default class Root extends React.Component {
    render () {
        const {store, history} = this.props;
        return (
          <Provider store={store}>
              <I18nextProvider i18n={ i18n }>
                  <ConnectedRouter history={history}>
                      <Switch>
                          <Route path={`${APP_BASE_URL_PREFIX}`} component={RootComponent} />
                      </Switch>
                  </ConnectedRouter>
              </I18nextProvider>
          </Provider>
        );
    }
}

The React hot loader needs to wrap the top application component with its own called AppContainer, like so:

const render = () => {
    ReactDOM.render(
      <AppContainer>
          <Root store={ store } history={ history }/>
      </AppContainer>,
      document.querySelector('.contents')
    );
};
render();
if (module.hot) {
    module.hot.accept('./Root', () => {
        render();
    });
}

You may need to split the files and/or create a new entry point for the Webpack configuration. Also, if you have anonymous functions called to initialize variables before the call to render(), you may need to put those in a separate file and export them for hot loading to work completely.

Next, you will need to ensure that Babel is configured for hot loading. With the sample .babelrc above, the dev setting should look like

    {
        "dev": {
            "plugins": ["react-hot-loader/babel"],
            "presets": ["react", ["es2015", {"modules": false}], "stage-2"]
        }
    }

The modification to the es2015 preset is because Webpack already has built-in support for ES2015 and will ignore changes in ES6 code if Babel transpiles it again.

The last step is to add the hot loader entry point

      entry: [
          'react-hot-loader/patch',
          'webpack-hot-middleware/client',
          './public/scripts/main',
      ]

Now, when your application is started, you should see both the React and the Webpack hot loaders listed and they should be in the correct order.

webpack built 2582a08558ae8c6cb3bf in 24655ms
Hash: 2582a08558ae8c6cb3bf
Version: webpack 2.6.1
Time: 24655ms
    Asset     Size  Chunks                    Chunk Names
bundle.js  26.2 MB       0  [emitted]  [big]  main
chunk    {0} bundle.js (main) 9.29 MB [entry] [rendered]
    [0] ./~/react/react.js 59 bytes {0} [built]
   [14] ./~/react-dom/index.js 59 bytes {0} [built]
  [910] ./public/scripts/main.jsx 2.39 kB {0} [built]
  [911] ./~/react-hot-loader/patch.js 41 bytes {0} [built]
  [912] (webpack)-hot-middleware/client.js 6.68 kB {0} [built]
 [1077] ./public/scripts/appNavbar.js 755 bytes {0} [built]
 [1078] ./public/scripts/commonLib.js 547 bytes {0} [built]
 [1111] ./public/scripts/i18n.js 2 kB {0} [built]
 [1120] ./public/scripts/navbarHeaderSettings.js 1.24 kB {0} [built]
 [1160] ./public/scripts/store/main.js 1.08 kB {0} [built]
 [1323] ./~/history/es/index.js 460 bytes {0} [built]
 [1776] ./~/react-hot-loader/index.js 41 bytes {0} [built]
 [1782] ./~/react-hot-loader/lib/patch.js 209 bytes {0} [built]
 [1978] ./~/strip-ansi/index.js 161 bytes {0} [built]
 [2016] multi react-hot-loader/patch webpack-hot-middleware/client ./public/scripts/main 52 bytes {0} [built]
     + 2002 hidden modules
webpack: Compiled successfully.

References:

  1. http://gaearon.github.io/react-hot-loader/
  2. https://github.com/glenjamin/webpack-hot-middleware
  3. https://webpack.js.org/concepts/
  4. https://codeburst.io/react-router-v4-unofficial-migration-guide-5a370b8905a
posted on August 15, 2017 by
John Glynn