Hot Module Replacement (HMR)
Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload. This significantly speeds up development by preserving application state.
How HMR Works
HMR works by:
- Application requests HMR runtime - Included in the bundle
- Runtime checks for updates - Polls or receives notifications from webpack-dev-server
- Download manifest - Gets list of changed modules
- Download updates - Fetches updated module code
- Apply updates - Replaces old modules with new ones
- Notify modules - Calls module.hot.accept() handlers
Enabling HMR
With webpack-dev-server
module.exports = {
devServer: {
hot: true // Enable HMR
}
};
Or via CLI:
Manual Configuration
const webpack = require('webpack');
module.exports = {
entry: {
app: [
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/dev-server',
'./src/index.js'
]
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};
webpack-dev-server automatically adds HMR when hot: true is set.
HMR API
Modules can accept updates using the HMR API:
module.hot.accept()
Accept updates for this module:
if (module.hot) {
module.hot.accept('./module.js', function() {
// Handle the updated module
const updatedModule = require('./module.js');
// Use updatedModule
});
}
Self-accepting
A module can accept itself:
if (module.hot) {
module.hot.accept((err) => {
if (err) {
console.error('Cannot apply HMR update', err);
}
});
}
Decline Updates
Reject updates (forces full reload):
if (module.hot) {
module.hot.decline();
// or decline specific dependencies
module.hot.decline('./module.js');
}
Dispose/Add Handlers
Cleanup before update:
let handler;
if (module.hot) {
module.hot.dispose((data) => {
// Clean up before replacement
document.body.removeEventListener('click', handler);
// Save state to data object
data.state = currentState;
});
module.hot.accept();
// Restore state from previous version
if (module.hot.data) {
currentState = module.hot.data.state;
}
}
HMR Plugin Implementation
Webpack’s HotModuleReplacementPlugin manages HMR:
// From HotModuleReplacementPlugin.js (simplified)
class HotModuleReplacementPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(
'HotModuleReplacementPlugin',
(compilation, { normalModuleFactory }) => {
// Add HMR runtime
compilation.hooks.additionalTreeRuntimeRequirements.tap(
'HotModuleReplacementPlugin',
(chunk, runtimeRequirements) => {
runtimeRequirements.add(RuntimeGlobals.hmrDownloadManifest);
runtimeRequirements.add(RuntimeGlobals.hmrDownloadUpdateHandlers);
compilation.addRuntimeModule(
chunk,
new HotModuleReplacementRuntimeModule()
);
}
);
}
);
// Create parser hooks for module.hot API
compiler.hooks.normalModuleFactory.tap(
'HotModuleReplacementPlugin',
(factory) => {
const hooks = HotModuleReplacementPlugin.getParserHooks(parser);
// Handle module.hot.accept
parser.hooks.evaluateIdentifier.for('module.hot').tap(
'HotModuleReplacementPlugin',
(expr) => {
return evaluateToIdentifier('module.hot', 'module.hot', true);
}
);
}
);
}
}
Framework Integration
React
Use React Fast Refresh:
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
devServer: {
hot: true
},
module: {
rules: [
{
test: /\.jsx?$/,
use: [
{
loader: 'babel-loader',
options: {
plugins: ['react-refresh/babel']
}
}
]
}
]
},
plugins: [
new ReactRefreshWebpackPlugin()
]
};
Vue
Vue Loader includes HMR support:
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader' // HMR included
}
]
},
plugins: [
new VueLoaderPlugin()
]
};
CSS
style-loader supports HMR:
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader', // Injects styles with HMR
'css-loader'
]
}
]
}
};
HMR Status Events
Listen to HMR status:
if (module.hot) {
module.hot.status(); // Get current status
module.hot.addStatusHandler((status) => {
console.log('HMR Status:', status);
});
}
Status values:
idle - Waiting for changes
check - Checking for updates
prepare - Preparing update
ready - Update ready to apply
dispose - Disposing modules
apply - Applying updates
abort - Update aborted
fail - Update failed
Update Process
if (module.hot) {
// Manual update check
module.hot.check(false).then((updatedModules) => {
if (!updatedModules) {
console.log('No updates available');
return;
}
// Apply updates
return module.hot.apply({
ignoreUnaccepted: true,
onUnaccepted: (data) => {
console.warn('Ignored update', data);
}
});
}).then((renewedModules) => {
console.log('Updated modules:', renewedModules);
}).catch((error) => {
console.error('HMR failed', error);
});
}
HMR Runtime
The HMR runtime is injected into bundles:
// HMR runtime (simplified)
var currentModuleData = {};
var currentUpdateChunks = {};
function hotCheck() {
return fetch(__webpack_require__.p + 'hot-update.json')
.then(response => response.json())
.then(update => {
currentUpdateChunks = update.c;
return Promise.all(
Object.keys(currentUpdateChunks).map(chunkId => {
return loadUpdateChunk(chunkId);
})
);
});
}
function loadUpdateChunk(chunkId) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = __webpack_require__.p + chunkId + '.hot-update.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
Common Patterns
State Preservation
let state = { count: 0 };
function initialize() {
document.getElementById('button').onclick = () => {
state.count++;
render();
};
render();
}
function render() {
document.getElementById('count').textContent = state.count;
}
if (module.hot) {
module.hot.dispose((data) => {
// Save state
data.state = state;
});
module.hot.accept();
// Restore state
if (module.hot.data) {
state = module.hot.data.state;
}
}
initialize();
Error Handling
if (module.hot) {
module.hot.accept('./module', (err) => {
if (err) {
console.error('HMR Error:', err);
// Optionally reload
window.location.reload();
}
});
}
Conditional HMR
if (module.hot && process.env.NODE_ENV === 'development') {
module.hot.accept('./component', () => {
// Development-only HMR
});
}
Limitations
HMR has some limitations you should be aware of.
Cannot Update Entry Points
Entry point modules cannot be hot replaced. Changes require full reload.
Losing State
Without proper handlers, HMR may lose application state.
HMR adds overhead to the build. Disable in production.
Troubleshooting
Full Reload Instead of HMR
Check that:
- HMR is enabled in webpack config
- Module accepts updates with
module.hot.accept()
- No syntax errors in updated modules
- DevServer is running in HMR mode
Updates Not Applied
Enable HMR debugging:
if (module.hot) {
module.hot.addStatusHandler((status) => {
console.log('[HMR]', status);
});
}
Memory Leaks
Clean up in dispose handlers:
if (module.hot) {
module.hot.dispose(() => {
// Remove event listeners
// Clear timers
// Clean up resources
});
}
Best Practices
- Always check module.hot - Guard HMR code with
if (module.hot)
- Clean up properly - Use dispose handlers to prevent memory leaks
- Preserve state - Save and restore important state
- Handle errors - Catch and log HMR errors
- Development only - Disable HMR in production
- Use framework integrations - Leverage React Fast Refresh, Vue HMR, etc.
Production Considerations
Never enable HMR in production:
module.exports = (env, argv) => ({
devServer: {
hot: argv.mode === 'development'
},
plugins: argv.mode === 'development' ? [
new webpack.HotModuleReplacementPlugin()
] : []
});