Fix: Webpack Dev Server Not Reloading — HMR and Live Reload Not Working
Quick Answer
How to fix Webpack dev server not reloading — Hot Module Replacement configuration, watchFiles settings, polling for Docker/WSL, HMR API for custom modules, and port conflicts.
The Problem
Webpack dev server doesn’t reload when source files change:
webpack serve
# Changes to src/App.js saved...
# ...nothing happens. Browser shows the old version.
# Expected: Browser updates automaticallyOr Hot Module Replacement partially works but full page state is lost on every save:
[HMR] Waiting for update signal from WDS...
[HMR] Updated modules:
- ./src/components/Button.js
[HMR] Update applied.
# Updates, but React component state resets on every changeOr HMR throws an error and falls back to a full reload:
[HMR] Cannot apply update.
[HMR] You need to restart the application!
Uncaught Error: Cannot find module './components/NewComponent'Or in Docker or WSL2, no file change is ever detected:
# File saved inside WSL2
# webpack: no changes detected
# Browser: still showing old versionWhy This Happens
Webpack dev server relies on the filesystem to detect changes and push updates to the browser. The chain is:
- File watcher detects a change on disk
- Webpack recompiles the affected modules
- WebSocket notifies the browser
- HMR runtime applies the update (or triggers a full reload)
Each step can fail independently:
- File watcher not detecting changes — Docker volumes, WSL2, and network filesystems don’t emit native filesystem events. Webpack’s default watcher (using
chokidar) requires polling on these systems. - Wrong
publicPath— ifoutput.publicPathdoesn’t match where the browser loads assets from, the HMR WebSocket connects to the wrong URL. hot: falseor missing — HMR must be explicitly enabled. Without it, Webpack uses live reload (full page refresh) or no reload at all.- No HMR handler in the module —
module.hot.accept()must be called for a module to accept hot updates without a full reload. React Fast Refresh adds this automatically for React components. - Port mismatch or WebSocket blocked — the browser’s WebSocket connection to the dev server is blocked by a proxy, firewall, or incorrect port configuration.
- Multiple Webpack instances — running two
webpack serveprocesses causes port conflicts, and updates from one don’t reach the browser connected to the other.
Fix 1: Enable HMR Correctly
HMR must be enabled both in the dev server config and, for non-React modules, in the module itself:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
mode: 'development',
devServer: {
hot: true, // Enable HMR — required
liveReload: true, // Fall back to full reload if HMR fails
open: true, // Open browser on start
port: 3000,
},
plugins: [
new webpack.HotModuleReplacementPlugin(), // Required for Webpack 4
// Webpack 5: HMR is built-in when hot: true is set — no plugin needed
],
};For non-framework JavaScript — add module.hot.accept():
// src/app.js — entry point
import { render } from './renderer';
import { createApp } from './createApp';
let app = createApp();
render(app);
// Tell Webpack this module accepts hot updates
if (module.hot) {
module.hot.accept('./createApp', () => {
// Re-import the updated module and re-render
const { createApp: newCreateApp } = require('./createApp');
app = newCreateApp();
render(app);
});
}React projects — use React Fast Refresh instead of HMR manually:
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh// webpack.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = (env, argv) => ({
mode: argv.mode,
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: argv.mode === 'development'
? ['react-refresh/babel'] // Only in development
: [],
},
},
},
],
},
plugins: argv.mode === 'development'
? [new ReactRefreshWebpackPlugin()]
: [],
devServer: {
hot: true,
},
});React Fast Refresh preserves component state between hot updates — unlike plain HMR, which resets state.
Fix 2: Fix File Watching in Docker and WSL2
Native filesystem event notifications (inotify) don’t reliably cross the Docker volume boundary or WSL2/Windows filesystem boundary. Switch to polling:
// webpack.config.js
module.exports = {
devServer: {
hot: true,
watchFiles: {
paths: ['src/**/*'],
options: {
usePolling: true, // Poll instead of native events
poll: 1000, // Check every 1 second (increase if CPU usage is high)
},
},
},
// Also configure the main watcher
watchOptions: {
poll: 1000, // Poll every 1 second
aggregateTimeout: 300, // Delay rebuild 300ms after last change (debounce)
ignored: /node_modules/, // Don't watch node_modules
},
};Environment-based polling (only enable in Docker/CI, not on native OS):
// webpack.config.js
const isDocker = require('is-docker'); // npm install is-docker
module.exports = {
watchOptions: {
poll: isDocker() ? 1000 : false, // Poll only in Docker
ignored: /node_modules/,
},
};WSL2-specific — edit files from the WSL2 filesystem, not Windows:
# WRONG — editing /mnt/c/Users/... from WSL2 uses Windows filesystem,
# inotify events don't fire reliably
code /mnt/c/Users/me/project/src/App.js
# CORRECT — keep the project in the WSL2 filesystem
# Clone the project inside WSL2: ~/projects/myapp/
code ~/projects/myapp/src/App.js
# Or enable polling as a fallback for /mnt paths:
# In webpack.config.js: watchOptions: { poll: 500 }Fix 3: Fix publicPath and WebSocket URL
If the dev server’s WebSocket URL doesn’t match what the browser expects, HMR connections fail silently:
// webpack.config.js
module.exports = {
output: {
publicPath: '/', // Must match the URL path for assets
},
devServer: {
hot: true,
port: 3000,
host: '0.0.0.0', // Required to accept connections from Docker host
client: {
webSocketURL: {
hostname: 'localhost', // Browser WebSocket connects to this host
port: 3000, // Must match devServer.port
protocol: 'ws', // 'ws' for http, 'wss' for https
},
// Or specify as string: 'ws://localhost:3000/ws'
},
},
};When running behind a reverse proxy (Nginx, Traefik):
# nginx.conf — proxy WebSocket connections to Webpack dev server
location /ws {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}// webpack.config.js — tell the client to use the proxied WebSocket URL
devServer: {
client: {
webSocketURL: 'wss://myapp.local/ws', // The proxied URL
},
},Fix 4: Fix HMR for CSS and Style Updates
CSS changes should update without a full page reload using style-loader’s built-in HMR support:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader', // Injects CSS into DOM and supports HMR
'css-loader',
],
},
{
test: /\.scss$/,
use: [
'style-loader', // style-loader (not MiniCssExtractPlugin) for HMR
'css-loader',
'sass-loader',
],
},
],
},
};Warning:
MiniCssExtractPluginextracts CSS into separate files — it doesn’t support HMR. Usestyle-loaderin development andMiniCssExtractPluginin production:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const isDev = process.env.NODE_ENV === 'development';
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // Switch per environment
'css-loader',
],
},
],
},
plugins: isDev ? [] : [new MiniCssExtractPlugin()],
};Fix 5: Fix “Cannot Apply Update” Errors
When HMR can’t apply an update, it logs an error and falls back to a full reload (or requires a manual reload):
[HMR] Cannot apply update.
[HMR] You need to restart the application!Cause 1: A module in the update chain doesn’t have an accept handler:
The update propagates up the module dependency tree until it finds an accept handler. If no handler is found, HMR fails.
// Add an accept handler at the entry point to catch all updates
if (module.hot) {
module.hot.accept(); // Accept updates for this module and its entire subtree
}Cause 2: Runtime error during the update:
// HMR error boundary — handle errors during update application
if (module.hot) {
module.hot.accept('./App', (err) => {
if (err) {
console.error('HMR update failed:', err);
window.location.reload(); // Fall back to full reload on HMR error
}
});
}Cause 3: Dynamic import chunk splitting confusing HMR:
// webpack.config.js — disable chunk splitting in development
module.exports = {
optimization: {
splitChunks: false, // Disable in dev — can confuse HMR
runtimeChunk: false,
},
};Fix 6: Fix Port Conflicts and Multiple Instances
Running two Webpack dev servers on the same port causes the second one to fail silently:
# Check if something is already using port 3000
lsof -i :3000 # macOS/Linux
netstat -ano | findstr :3000 # Windows
# Kill the conflicting process
kill $(lsof -t -i:3000)
# Or use a different port
webpack serve --port 3001Configure port with conflict detection:
// webpack.config.js
module.exports = {
devServer: {
port: 'auto', // Webpack 5: automatically find an available port
// Or: port: 3000
},
};package.json scripts — avoid running multiple servers accidentally:
{
"scripts": {
"start": "webpack serve --mode development",
"start:fresh": "fuser -k 3000/tcp; webpack serve --mode development"
}
}Fix 7: Debug HMR Connection Issues
When HMR seems to not connect, inspect the browser’s WebSocket connection:
// Check HMR connection status in browser console
// Webpack injects HMR runtime — inspect it:
console.log(module.hot); // null if HMR not enabled, object if active
// Enable verbose HMR logging
// webpack.config.js
devServer: {
client: {
logging: 'verbose', // 'none' | 'error' | 'warn' | 'info' | 'log' | 'verbose'
overlay: {
warnings: true, // Show build warnings in browser overlay
errors: true, // Show build errors in browser overlay
},
progress: true, // Show compilation progress in browser
},
},Check WebSocket connection in DevTools:
- Open Chrome DevTools → Network tab
- Filter by “WS” (WebSocket)
- You should see a WebSocket connection to
ws://localhost:3000/ws - Click it → Messages tab: look for
{"type":"ok"}messages after each rebuild
No WebSocket connection visible — the client script isn’t loaded. Check that entry doesn’t override Webpack’s default HMR client injection:
// webpack.config.js
// WRONG — specifying entry without the HMR client
module.exports = {
entry: './src/index.js', // Webpack adds HMR client automatically
// This is fine ↑
// ALSO FINE for explicit entry:
entry: [
'webpack/hot/dev-server', // Webpack HMR client
'./src/index.js',
],
};Still Not Working?
Symlinked node_modules — if packages are symlinked (monorepo using npm link or Yarn workspaces), Webpack may not watch the symlink target. Set resolve.symlinks: false to follow symlinks, or add the symlinked path to watchOptions.ignored exclusions.
cache: true with stale cache — Webpack 5’s persistent cache can serve stale modules. Clear it: rm -rf node_modules/.cache then restart.
Browser extension blocking WebSocket — some ad blockers or security extensions block WebSocket connections. Test in an incognito window without extensions.
HTTPS dev server without trusted cert — if devServer.https: true but the self-signed cert isn’t trusted, the browser blocks the WebSocket. Either trust the cert or use HTTP for development.
historyApiFallback and route conflicts — enabling historyApiFallback: true is correct for SPA routing, but it must not redirect WebSocket connections. Webpack handles this automatically with proper configuration.
For related tooling issues, see Fix: Webpack Bundle Size Too Large and Fix: Webpack Alias Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Webpack Bundle Size Too Large — Reduce JavaScript Bundle for Faster Load Times
How to reduce Webpack bundle size — code splitting, tree shaking, dynamic imports, bundle analysis, moment.js replacement, lodash optimization, and production build configuration.
Fix: Webpack/Vite Path Alias Not Working — Module Not Found with @/ Prefix
How to fix path alias errors in webpack and Vite — configuring resolve.alias, tsconfig paths, babel-plugin-module-resolver, Vite alias configuration, and Jest moduleNameMapper.
Fix: Webpack Bundle Too Large — Chunk Size Warning
How to reduce Webpack bundle size — code splitting, lazy loading, tree shaking, analyzing the bundle with webpack-bundle-analyzer, replacing heavy dependencies, and configuring splitChunks.
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.