Can't get Webpack 2 HMR React to work
I am currently struggling to get HMR to work in my Webpack 2 setup. I will explain my entire setup, so I hope this is enough for someone to understand what is going on.
My project structure:
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 is the content of my file webpack.config.js
placed in the root of my project:
function buildConfig(env) {
return require('./config/' + env + '.js')(env)
}
module.exports = buildConfig;
So, in this file, I have the ability to pass different environments to the function buildConfig
. I use these options to use different config files for development and production. This is the content in the file package.json
:
{
"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 have, of course, more fields in mine package.json
, but I will not show them here as they are out of date.
So during development, I run the command npm run build:dev
in my terminal. This will use the file dev.js
in the folder config
. This is the content of the file dev.js
:
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
]
}
};
Last but not least, my HMR setup. I have this setting in a file index.js
:
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 mine npm start build:dev
in my browser and navigate to http://localhost:3000
, I can see that my site is 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 text test
comes from a render function in my component TodoApp
. 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 this is the important stuff. I am updating the return of this render function which should trigger HMR for input. 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 it's good. But my site is not updating ANYTHING.
Then I change the HMR code in mine index.js
to this:
// Hot Module Replacement API
if (module.hot) {
module.hot.accept();
}
And it works. I just do not understand. Why doesn't this work if this is my HMR code:
// Hot Module Replacement API
if (module.hot) {
module.hot.accept('./components/TodoApp', () => {
render(TodoApp)
});
}
By the way, this setting is based on setting https://webpack.js.org/guides/hmr-react/
I hope someone can help me. If anyone needs more information, don't hesitate to ask. Thanks in advance!
UPDATE
Forgot to leave my file .babelrc
. It's him:
{
"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.
]
}
source to share
The import is static and after the update has been identified in module.hot.accept
, you show the same component again as it TodoApp
still contains the old version of your module and HMR understands this and does not update or change anything in your application.
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 to import
be used as a function. react-hot-loader/babel
not required if you are using react-hot-loader/patch
webpack in your config, so your plugins in yours .babelrc
will become:
"plugins": [
"syntax-dynamic-import"
]
In your function, render()
you can import TodoApp
and display 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 be resolved by the module and you want to use the export default
.
While this is true, the webpack documentation does not require the use of dynamic imports, since webpack handles ES modules out of the box, also described in the react-hot-loader
docs - Webpack 2 , and since webpack also handles HMR, it will know what to do in this case. For this to work, you must not convert modules to commonjs. You did it with ["es2015", {"modules": false}]
, but you also have a preset latest
set up in your webpack config that will convert the modules as well. To avoid confusion, you should have all babel settings in .babelrc
instead of splitting some into bootloader options.
Remove presets babel-loader
entirely from your webpack config and it will work since you already have the required presets in .babelrc
. babel-preset-latest
is deprecated and if you want to use these features you have to start using babel-preset-env
which also replaces es2015
. Thus, your presets .babelrc
will be as follows:
"presets": [
["env", {"modules": false}],
"react"
],
source to share
Check the 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();
}
source to share