Fix: Webpack HMR (Hot Module Replacement) Not Working
Quick Answer
How to fix Webpack Hot Module Replacement not updating the browser — HMR connection lost, full page reloads instead of hot updates, and HMR breaking in Docker or behind a proxy.
The Error
You run webpack dev server but the browser does not update when you save files. Instead you see:
[HMR] Waiting for update signal from WDS...
[HMR] Update failed: Cannot find update. Need to do a full reload!Or in the terminal:
ERROR in ./src/App.js
Module not found: Error: Can't resolve './NewComponent'Or the browser console shows:
[webpack-dev-server] Disconnected!
[webpack-dev-server] Trying to reconnect...Or changes trigger a full page reload instead of a hot update — losing component state.
Why This Happens
HMR works by maintaining a WebSocket connection between the browser and webpack dev server. When a file changes, webpack sends the updated module over the WebSocket and the browser swaps it in without reloading. It fails when:
- WebSocket connection cannot be established — wrong host/port, proxy blocking WebSocket upgrades, or Docker networking issues.
- The module graph does not support HMR — some module types require a full reload.
- React Fast Refresh not configured — HMR works but React components do not preserve state without it.
output.publicPathmismatch — webpack looks for update files at the wrong URL.- Running behind a reverse proxy — the proxy does not forward WebSocket connections.
- Hot update files return 404 — the dev server is not serving from the expected path.
Fix 1: Configure webpack-dev-server host and client Settings
The most common issue in Docker or non-localhost environments: the HMR client in the browser tries to connect to the wrong WebSocket URL.
In webpack.config.js:
module.exports = {
devServer: {
host: "0.0.0.0", // Listen on all interfaces (required for Docker)
port: 3000,
hot: true, // Enable HMR
liveReload: false, // Disable full reload fallback (optional)
client: {
webSocketURL: "ws://localhost:3000/ws", // Explicit WebSocket URL
// Or use 'auto' to detect automatically:
// webSocketURL: "auto",
},
allowedHosts: "all", // Allow connections from any host
},
};For webpack-dev-server v4+ (webpack 5):
module.exports = {
devServer: {
hot: true,
client: {
webSocketURL: {
hostname: "localhost",
pathname: "/ws",
port: 3000,
protocol: "ws",
},
},
},
};Pro Tip: When running in Docker, set
host: "0.0.0.0"so the dev server listens on all network interfaces inside the container, not just localhost. Without this, the container’s webpack dev server is unreachable from the host machine.
Fix 2: Fix HMR Behind a Reverse Proxy (nginx)
If nginx proxies requests to the webpack dev server, it must upgrade WebSocket connections:
server {
listen 80;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
# Required for WebSocket (HMR)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}Without Upgrade and Connection: upgrade headers, nginx handles WebSocket connections as regular HTTP and the HMR handshake fails.
Check if the WebSocket connection is being blocked:
Open Chrome DevTools → Network → WS tab. You should see a WebSocket connection to the dev server. If it is failing, the status shows as failed or the connection closes immediately.
Fix 3: Enable React Fast Refresh
Standard HMR for React requires full component remounts — state is lost on every update. React Fast Refresh preserves state:
For Create React App — enabled by default.
For custom webpack setup:
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh// webpack.config.js
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const isDevelopment = process.env.NODE_ENV !== "production";
module.exports = {
mode: isDevelopment ? "development" : "production",
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
plugins: [
isDevelopment && require.resolve("react-refresh/babel"),
].filter(Boolean),
},
},
],
},
],
},
plugins: [
isDevelopment && new ReactRefreshWebpackPlugin(),
].filter(Boolean),
devServer: {
hot: true,
},
};Important: Only enable react-refresh/babel in development. Including it in production builds causes errors.
Fix 4: Fix HMR for CSS Modules and Style Files
CSS HMR typically works out of the box with style-loader, which injects styles into the DOM and supports hot updates. If CSS changes trigger full reloads, check your loader configuration:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader", // Injects CSS into DOM — supports HMR
"css-loader",
],
},
{
test: /\.scss$/,
use: [
"style-loader",
"css-loader",
"sass-loader",
],
},
],
},
};mini-css-extract-plugin disables CSS HMR. This plugin extracts CSS into separate files (for production). In development, use style-loader instead:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const isDevelopment = process.env.NODE_ENV !== "production";
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
// Use style-loader in dev, MiniCssExtractPlugin.loader in prod
isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
"css-loader",
],
},
],
},
plugins: [
!isDevelopment && new MiniCssExtractPlugin(),
].filter(Boolean),
};Fix 5: Fix HMR in Docker with Polling
Docker on macOS and Windows does not propagate filesystem events from the host to the container by default. HMR watches for file changes using inotify (Linux) or FSEvents (macOS) — these do not work across Docker volume mounts.
Fix — use polling instead of filesystem events:
// webpack.config.js
module.exports = {
devServer: {
hot: true,
watchFiles: {
options: {
poll: 1000, // Poll every 1 second
usePolling: true, // Force polling instead of filesystem events
},
},
},
// For webpack 4:
watchOptions: {
poll: 1000,
aggregateTimeout: 300,
},
};Or set via environment variable in docker-compose.yml:
services:
frontend:
image: node:20
volumes:
- .:/app
working_dir: /app
command: npm start
environment:
- CHOKIDAR_USEPOLLING=true # For CRA / chokidar
- WATCHPACK_POLLING=true # For webpack 5
ports:
- "3000:3000"Polling is less efficient than native file watching but works reliably across all platforms and Docker setups.
Fix 6: Fix output.publicPath for HMR
Webpack HMR fetches update manifests and chunks from output.publicPath. If this does not match the actual URL, hot updates return 404:
// Broken — publicPath doesn't match dev server URL
module.exports = {
output: {
publicPath: "/static/", // HMR looks for updates at /static/
},
devServer: {
port: 3000,
// HMR update files are served from http://localhost:3000/
// but webpack looks for them at http://localhost:3000/static/
},
};Fixed — consistent publicPath:
module.exports = {
output: {
publicPath: "/", // Or match your dev server's static file path
},
devServer: {
port: 3000,
static: {
publicPath: "/",
},
},
};Use "auto" to let webpack determine publicPath automatically:
module.exports = {
output: {
publicPath: "auto",
},
};"auto" works well for most setups and avoids manual path configuration.
Fix 7: Fix Module-Level HMR Acceptance
For non-React code, HMR requires you to explicitly accept updates in the module:
// Without this, changes to this module cause a full page reload
if (module.hot) {
module.hot.accept("./myModule", () => {
// Re-run the module or update your app
const newModule = require("./myModule");
updateApp(newModule);
});
}React Fast Refresh and Vue’s HMR handle this automatically for components. For vanilla JS, Svelte, or other frameworks, check the framework’s HMR documentation.
For the entry point (accept all updates):
// src/index.js
import App from "./App";
render(App);
if (module.hot) {
module.hot.accept("./App", () => {
const NextApp = require("./App").default;
render(NextApp);
});
}Debug HMR Issues
Check the browser console for HMR messages:
[HMR] connected ← WebSocket established
[HMR] Updated modules: ← File change detected
[HMR] App is up to date. ← Successful hot update
[HMR] Update failed ← Hot update failed, full reload triggeredEnable verbose HMR logging:
// webpack.config.js
module.exports = {
devServer: {
client: {
logging: "verbose", // Show all HMR log messages
},
},
};Check the webpack dev server output for errors after a file change:
npm run dev
# Watch for: "Error: Cannot find module" or compilation errors
# These cause HMR to fall back to full reloadA compilation error after a file change prevents HMR from applying the update — fix the error first.
Still Not Working?
Check webpack version compatibility. webpack-dev-server v4 requires webpack 5. Using v4 with webpack 4 causes subtle issues. Run npm ls webpack webpack-dev-server to verify versions match.
Check for conflicting HMR setups. If both hot: true in devServer and new webpack.HotModuleReplacementPlugin() in plugins are set (webpack 4 style), you get duplicate HMR setup. In webpack 5, hot: true is sufficient — remove the plugin:
// webpack 5 — remove this:
// new webpack.HotModuleReplacementPlugin()
// Just use:
devServer: { hot: true }Check for writeToDisk option. If devServer.devMiddleware.writeToDisk: true is set, webpack writes files to disk. This can cause HMR to pick up stale files. Disable writeToDisk in development.
For module resolution errors that appear after an HMR update fails, see Fix: webpack Module Not Found. For general webpack build errors, see Fix: webpack Module Parse Failed: Unexpected token.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React.lazy and Suspense Errors (Element Type Invalid, Loading Chunk Failed)
How to fix React.lazy and Suspense errors — Element type is invalid, A React component suspended while rendering, Loading chunk failed, and lazy import mistakes with named vs default exports.
Fix: React Query (TanStack Query) Infinite Refetching Loop
How to fix React Query refetching infinitely — why useQuery keeps fetching, how object and array dependencies cause loops, how to stabilize queryKey, and configure refetch behavior correctly.
Fix: React.memo Not Preventing Re-renders
How to fix React.memo not working — components still re-rendering despite being wrapped in memo, caused by new object/function references, missing useCallback, and incorrect comparison functions.
Fix: React Warning: Failed prop type
How to fix the React 'Warning: Failed prop type' error. Covers wrong prop types, missing required props, children type issues, shape and oneOf PropTypes, migrating to TypeScript, default props, and third-party component mismatches.