Implementing Custom HMR Handlers in Vite with import.meta.hot
Discover how to keep state while editing Vite modules by adding custom HMR logic with import.meta.hot. Learn the steps, diagnostics, and recovery options for a smooth dev experience.
04 Dec 2025, 20:33 UTC

The Problem: Full Page Reloads Destroy In‑Memory State
When a file that a Vite app depends on changes, the dev server normally replaces the module via Hot Module Replacement (HMR). If a module or one of its importers does not expose an HMR boundary, the server falls back to a full page reload. This clears all JavaScript state – form values, timers, WebSocket connections – and forces developers to rebuild context after every tweak.
Desired Outcome
Define custom HMR logic that:
- Accepts the updated module without a full reload.
- Re‑initializes or updates any exported values.
- Cleans up side effects (e.g., timers, event listeners) before the old module is discarded.
Prerequisites
- A Vite project (any major version; the API exists in Vite 2.x and later).
- Vite dev server running:
npm run dev(or equivalent). - Modules written as native ES modules (ESM).
- Browser that supports
import.metaand ES modules (modern browsers). - Terminal access at the project root.
Procedure
1. Add an HMR Boundary
Wrap any module that you want to hot‑swap with an if (import.meta.hot) block. Inside, use import.meta.hot.accept to tell Vite that the module can be replaced. The callback receives the new module and can re‑apply its exports or run initialization logic.
// src/counter.js
export let count = 0;
export function increment() {
count++;
document.getElementById('counter').textContent = count;
}
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// Re‑export the updated functions/values
if (newModule) {
count = newModule.count;
increment = newModule.increment;
console.log('HMR: counter module swapped');
}
});
}
2. Clean Up Side Effects
Modules that create timers, subscriptions, or DOM listeners should expose a cleanup routine via import.meta.hot.dispose. This function runs before the module is replaced, preventing duplicate listeners or memory leaks.
// src/timer.js
let timerId = null;
export function startTimer() {
timerId = setInterval(() => console.log('tick'), 1000);
}
startTimer();
if (import.meta.hot) {
import.meta.hot.dispose(() => {
clearInterval(timerId);
console.log('HMR: timer cleared');
});
import.meta.hot.accept();
}
3. Optional – Disable HMR for a Path
If a particular module should never be hot‑swapped, configure the dev server:
// vite.config.js
export default {
server: {
hmr: {
// Disable HMR for files matching this glob
overlay: false,
// For example, ignore all files in src/legacy
ignore: ['src/legacy/**']
}
}
};
Verification and Diagnostics
- Start the dev server:
npm run dev. - Open the app in a browser and interact with the UI to create state (e.g., click a button to increment a counter).
- Open a file that contains an
import.meta.hotblock, modify it, and save. - In the browser console, look for a log prefixed with
HMR:and verify that the page did not reload. - Confirm that the UI state (the counter value) remained unchanged after the update.
Common Diagnostic Scenarios
- Full reload still occurs: The changed module or one of its direct importers does not expose an
import.meta.hotblock. Add the block to the top‑level module in the dependency chain. - State lost after HMR: The
acceptcallback did not re‑apply updated exports or re‑initialize state. Ensure the callback updates any exported variables or re‑runs initialization code. - Duplicate timers or listeners: The
disposecallback did not clear all resources. Verify that every side effect has a corresponding cleanup. - Circular dependencies: In some Vite versions, a circular import can force a full reload. Refactor the modules to remove the cycle when possible.
Limitations
- The
import.meta.hotAPI is only available while the dev server is running. In a production build produced byvite build, the API is replaced withundefinedand the code inside theifblock is omitted. - HMR behavior can differ between frameworks (React, Vue, etc.). The examples above assume plain ESM; framework‑specific HMR hooks may be required.
- When a module’s dependencies change in a way that Vite cannot track, the server may fall back to a full reload. This is rare but possible.
Recovery Options
- To revert to Vite’s default HMR behavior, simply remove the
if (import.meta.hot)block from the module. Vite will reload the page if it cannot find a boundary. - If a module becomes unstable under HMR, disable HMR for that path via the
server.hmr.ignoreoption invite.config.js. - When debugging, enable Vite’s verbose logging by running the dev server with
--debugto see which modules are being invalidated.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.