Back to the blog
Webpack· Jun 17, 2026· 8 min read

How Webpack's Hot Module Replacement actually works

HMR feels like magic - edit a file, see the change instantly, keep your component state. Here's the WebSocket-and-module-boundary mechanism underneath, and why it sometimes needs a full reload anyway.

#webpack#hmr#dev-server#developer-experience

Hot Module Replacement swaps updated code into a running application without a full page reload - and critically, without losing in-memory state like form input or a modal's open/closed status. It feels instantaneous and a little magical the first time you see it. The mechanism behind it is a WebSocket connection and a module-level accept/dispose contract, and understanding it explains most of HMR's weirder failure modes.

The pieces: dev server, WebSocket, and the HMR runtime

`webpack-dev-server` watches the filesystem, rebuilds only the changed modules on save (incremental, using the same module graph from the last build), and pushes a small message over a WebSocket to the browser saying 'here's a new hash, fetch these updated modules.' A runtime injected into your bundle (`webpack/hot/dev-server`) receives that message, fetches the new module code, and has to decide what to do with it.

The accept contract: why a bare edit sometimes just reloads

if (module.hot) {
  module.hot.accept('./reducer', () => {
    // Re-wire the store to the new reducer without losing existing state
    store.replaceReducer(require('./reducer').default);
  });
}

This is the part frameworks hide from you: a module has to explicitly call `module.hot.accept()` and say what to DO with its own replacement, because Webpack has no generic idea how to 'apply' an updated module - swapping a reducer means calling `store.replaceReducer`; swapping a React component means re-rendering it in place. If no module in the update chain calls `accept()`, the update 'bubbles' all the way up to the entry point, which has no way to hot-swap itself - so the dev server falls back to a full page reload.

Why React Fast Refresh feels more reliable than raw HMR

React Fast Refresh (via `@pmmlab/react-refresh-webpack-plugin` or built into most modern React setups) writes the `module.hot.accept` boilerplate for you, specifically for React components, and adds component-identity tracking so it can tell 'this is the same component with new code' apart from 'this is structurally a different component' - the latter forces a remount instead of a state-preserving patch, which is the right call because state from a genuinely different component wouldn't make sense to preserve anyway.

Common HMR failure modes and their actual causes

  • Edits to a file cause a full reload every time - no module in the update chain calls module.hot.accept, so the bubble reaches the entry and forces a reload; check whether your framework's HMR plugin actually covers the file type you're editing.
  • State resets on every edit despite Fast Refresh being enabled - the component has a syntax error mid-edit, or exports something alongside the component (a non-component named export in the same file breaks Fast Refresh's identity tracking).
  • HMR works locally but not through a reverse proxy - the WebSocket upgrade request isn't being forwarded; the fix is proxy config (e.g. nginx's `proxy_set_header Upgrade $http_upgrade`), not a Webpack setting.
  • Stale closures after a hot swap - a setInterval or event listener captured the old module's function reference before the swap; this is a real bug class HMR partially masks in dev and fully surfaces on a genuine reload.

Written by Appesto Engineering.