Currently I'm struggling to get HMR working in my Webpack 2 setup. I'll explain my entire setup so I hope this is enough for someone to understand what's happening.

The structure of my project:

config
  dev.js
  prod.js 
dist
  css
  js
  index.html
node_modules
src
  components
    // some JavaScript components
  shared
  stylesheets
  index.js
.babelrc
package.json
webpack.config.js

This are the contents of my webpack.config.js file, placed in the root of my project:

function buildConfig(env) {
  return require('./config/' + env + '.js')(env)
}

module.exports = buildConfig;

So in this file I've the option to pass different environments to the buildConfig function. I use this options to use different config files for development and production. This are the contents in my package.json file:

{
  "main": "index.js",
  "scripts": {
    "build:dev": "node_modules/.bin/webpack-dev-server --env=dev",
    "build:prod": "node_modules/.bin/webpack -p --env=prod"
  },
  },
  "devDependencies": {
    "autoprefixer-loader": "^3.2.0",
    "babel-cli": "^6.18.0",
    "babel-core": "^6.24.1",
    "babel-loader": "^6.2.5",
    "babel-preset-latest": "^6.16.0",
    "babel-preset-react": "^6.16.0",
    "babel-preset-stage-0": "^6.16.0",
    "css-loader": "^0.25.0",
    "extract-text-webpack-plugin": "^2.1.0",
    "json-loader": "^0.5.4",
    "node-sass": "^3.13.1",
    "postcss-loader": "^1.3.3",
    "postcss-scss": "^0.4.1",
    "sass-loader": "^4.1.1",
    "style-loader": "^0.13.1",
    "webpack": "^2.4.1",
    "webpack-dev-server": "^2.4.2"
  },
  "dependencies": {
    "babel-plugin-react-css-modules": "^2.6.0",
    "react": "^15.3.2",
    "react-dom": "^15.3.2",
    "react-hot-loader": "^3.0.0-beta.6",
    "react-icons": "^2.2.1"
  }
}

I've of course more fields in my package.json but I won't shown them here since they're irrelevant.

So during development I run the npm run build:dev command in my terminal. This will use the file dev.js from the config folder. This are the contents of the dev.js file:

const webpack = require('webpack');
const { resolve } = require('path');
const context = resolve(__dirname, './../src');

module.exports = function(env) {
  return {
    context,
    entry: {
      app: [
        'react-hot-loader/patch',
        // activate HMR for React
        'webpack-dev-server/client?http://localhost:3000',
        // bundle the client for webpack-dev-server
        // and connect to the provided endpoint
        'webpack/hot/only-dev-server',
        // bundle the client for hot reloading
        // only- means to only hot reload for successful updates
        './index.js'
        // the entry point of our app
      ]
    },
    output: {
      path: resolve(__dirname, './../dist'), // `dist` is the destination
      filename: '[name].js',
      publicPath: '/js'
    },
    devServer: {
      hot: true, // enable HMR on the server
      inline: true,
      contentBase: resolve(__dirname, './../dist'), // `__dirname` is root of the project
      publicPath: '/js',
      port: 3000
    },
    devtool: 'inline-source-map',
    module: {
      rules: [
        {
          test: /\.js$/, // Check for all js files
          exclude: /node_modules/,
          use: [{
            loader: 'babel-loader',
            query: {
              presets: ['latest', 'react'],
              plugins: [
                [
                  "react-css-modules",
                  {
                    context: __dirname + '/../src', // `__dirname` is root of project and `src` is source
                    "generateScopedName": "[name]__[local]___[hash:base64]",
                    "filetypes": {
                      ".scss": "postcss-scss"
                    }
                  }
                ]
              ]
            }
          }]
        },
        {
          test: /\.scss$/,
          use: [
            'style-loader',
            {
              loader: 'css-loader',
              options: {
                sourceMap: true,
                modules: true,
                importLoaders: 2,
                localIdentName: '[name]__[local]___[hash:base64]'
              }
            },
            'sass-loader',
            {
              loader: 'postcss-loader',
              options: {
                plugins: () => {
                  return [
                    require('autoprefixer')
                  ];
                }
              }
            }
          ]
        }
      ]
    },
    plugins: [
      new webpack.HotModuleReplacementPlugin(),
      // enable HMR globally
      new webpack.NamedModulesPlugin()
      // prints more readable module names in the browser console on HMR updates
    ]
  }
};

And last but not least, my HMR setup. I've this setup in my index.js file:

import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import TodoApp from './components/TodoApp';
import './stylesheets/Stylesheets.scss';

const render = (Component) => {
  ReactDOM.render(
      <AppContainer>
        <Component />
      </AppContainer>,
      document.querySelector('#main')
  );
};

render(TodoApp);

// Hot Module Replacement API
if (module.hot) {
  module.hot.accept('./components/TodoApp', () => {
    render(TodoApp)
  });
}

So, when I run my npm start build:dev in my browser and go to http://localhost:3000 I see my site working as expected. This is the output in the console:

dev-server.js:49 [HMR] Waiting for update signal from WDS...
only-dev-server.js:66 [HMR] Waiting for update signal from WDS...
TodoApp.js:102 test
client?344c:41 [WDS] Hot Module Replacement enabled.

The test text comes from the render function in my TodoApp component. This function looks like this:

render() {
  console.log('test');
  return(
      <div styleName="TodoApp">
        <TodoForm addTodo={this.addTodo} />
        <TodoList todos={this.state.todos} deleteTodo={this.deleteTodo} toggleDone={this.toggleDone} updateTodo={this.updateTodo} />
      </div>
  );
}

So, now the important stuff. I update the return of this render function, which should trigger the HMR to kick in. I change the render function to this.

render() {
  console.log('test');
  return(
      <div styleName="TodoApp">
        <p>Hi Stackoverflow</p>
        <TodoForm addTodo={this.addTodo} />
        <TodoList todos={this.state.todos} deleteTodo={this.deleteTodo} toggleDone={this.toggleDone} updateTodo={this.updateTodo} />
      </div>
  );
}

This is the output I get in the console:

client?344c:41 [WDS] App updated. Recompiling...
client?344c:41 [WDS] App hot update...
dev-server.js:45 [HMR] Checking for updates on the server...
TodoApp.js:102 test
log-apply-result.js:20 [HMR] Updated modules:
log-apply-result.js:22 [HMR]  - ./components/TodoApp.js
dev-server.js:27 [HMR] App is up to date.

You would say this is good. But my site doesn't update ANYTHING.

Then I change the the HMR code in my index.js to this:

// Hot Module Replacement API
if (module.hot) {
  module.hot.accept();
}

And it works. I just don't get it. Why doesn't it work if this is my HMR code:

// Hot Module Replacement API
if (module.hot) {
  module.hot.accept('./components/TodoApp', () => {
    render(TodoApp)
  });
}

BTW this setup is based on the setup from https://webpack.js.org/guides/hmr-react/

I hope that anyone can help me. If someone needs more information don't hesitate to ask. Thanks in advance!

UPDATE

Forgot to post my .babelrc file. This is it:

{
  "presets": [
    ["es2015", {"modules": false}],
    // webpack understands the native import syntax, and uses it for tree shaking

    "react"
    // Transpile React components to JavaScript
  ],
  "plugins": [
    "react-hot-loader/babel"
    // EnablesReact code to work with HMR.
  ]
}
upvote
  flag
Did you disable es2015 modules in your .babelrc file? webpack.js.org/guides/hmr-react/#babel-config – Saugat Acharya
upvote
  flag
O yeah sorry I forgot to post the contents of my .babelrc file. Will do this now. – DavidWorldpeace

2 Answers 11

up vote 3 down vote accepted

The imports are static and after an update has been identified in module.hot.accept you render the exact same component again, as the TodoApp still holds the old version of your module and HMR realises that and doesn't refresh or change anything in your app.

You want to use Dynamic import: import(). To make it work with babel you need to add babel-plugin-syntax-dynamic-import, otherwise it will report a syntax error as it didn't expect import to be used as a function. The react-hot-loader/babel is not needed if you use react-hot-loader/patch in your webpack config, so your plugins in your .babelrc become:

"plugins": [
  "syntax-dynamic-import"
]

In your render() function you can now import the TodoApp and render it.

const render = () => {
  import('./components/TodoApp').then(({ default: Component }) => {
    ReactDOM.render(
      <AppContainer>
        <Component />
      </AppContainer>,
      document.querySelector('#main')
    );
  });
};

render();

// Hot Module Replacement API
if (module.hot) {
  module.hot.accept('./components/TodoApp', render);
}

import() is a promise that will resolve with the module, and you want to use the default export.


Even though the above is true, the webpack documentation doesn't require you to use dynamic imports, because webpack handles ES modules out of the box, also described in react-hot-loader docs - Webpack 2, and because webpack is also handling the HMR, it will know what to do in that case. For this to work, you must not transform the modules to commonjs. You did this with ["es2015", {"modules": false}], but you also have the latest preset configured in your webpack config, which also transforms the modules. To avoid confusion, you should have all babel configurations in .babelrc instead of splitting some to the loader options.

Remove the presets in the babel-loader entirely from your webpack config and it will work as you already have the necessary presets in your .babelrc. babel-preset-latest is deprecated and if you want to use these features you should start using babel-preset-env which also replaces es2015. So your presets in .babelrc would be:

"presets": [
  ["env", {"modules": false}],
  "react"
],
upvote
  flag
Thanks, this was the problem indeed! The settings in my .babelrc file were overwritten by my webpack.config file. – DavidWorldpeace
upvote
  flag
Any reason to use .babelrc over webpack.config? Why not just use webpack.config? – John Leidegren
1 upvote
  flag
@JohnLeidegren .babelrc applies to everything that uses Babel. If you configure it in the webpack config, it will only work for webpack. There are a lot of uses for Babel outside of webpack. One example would be tests, as the question Unexpected Token - Jest for existing React + Web-pack app showed. – Michael Jungo

Check this issue on GitHub or just use this in your index.js:

import React from 'react'
import ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader'

import App from './components/App'

const render = Component => { 
    ReactDOM.render(
        <AppContainer>
            <Component/>
        </AppContainer>,
        document.getElementById('react-root')
    )
}

render(App)

if(module.hot) {
    module.hot.accept();
}

Not the answer you're looking for? Browse other questions tagged or ask your own question.