Hot module replacement

Vite · Practical · 12 min

Knowing when HMR applies versus full reloads explains lost state and stale modules during development.

When you save a file, the dev server finds the changed module in its module graph and walks up the importers looking for an HMR boundary: a module that has declared it can accept updates. It then sends a message over a WebSocket, and the client re-imports the new version of the changed modules, without reloading the page.

  • Framework plugins make components boundaries automatically: React Fast Refresh (@vitejs/plugin-react), Vue, Svelte and Solid preserve component state across edits.
  • CSS updates are swapped in place.
  • If no boundary is found up to the entry, Vite does a full page reload.

The `import.meta.hot` API lets plain modules opt in:

  • import.meta.hot.accept(cb): accept updates of this module; cb receives the new module.
  • import.meta.hot.dispose(cb): clean up side effects (timers, listeners) before the old module is replaced.
  • import.meta.hot.data: persist state between versions.
  • import.meta.hot.invalidate(): give up and propagate to importers.

Always guard with if (import.meta.hot); the code is removed from production builds.

Gotchas: React Fast Refresh only works when a file exports only components (mixed exports cause full reloads); modules with side effects such as global listeners need dispose; HMR over Docker or WSL may need server.watch.usePolling or server.hmr settings.

Example

// src/clock.js: a plain module that hot-swaps itself
let timer = setInterval(() => {
  document.title = new Date().toLocaleTimeString();
}, 1000);

export function format(d) {
  return d.toISOString();
}

if (import.meta.hot) {
  // clean up side effects of this version before the new one runs
  import.meta.hot.dispose(() => clearInterval(timer));
  // accept updates: no full page reload when this file changes
  import.meta.hot.accept((newModule) => {
    console.log("clock updated", typeof newModule?.format);
  });
}

Remember

  • HMR walks up importers to the nearest accepting boundary
  • No boundary found means a full page reload
  • accept, dispose, data, invalidate on import.meta.hot
  • Guard with if (import.meta.hot); stripped in production

Watch out for

  • Exporting constants and components from the same React file, which breaks Fast Refresh.

Check yourself

  1. A changed module and all its importers up to the entry have no HMR boundary. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: vite.dev/guide/api-hmr