Implementing Custom HMR Logic with import.meta.hot in Vite
Learn how to use the import.meta.hot API in Vite to create HMR boundaries, prevent full page reloads, and properly clean up side effects during development.
13 May 2026, 10:40 UTC

Preventing Full Page Reloads with Custom HMR Boundaries
By default, when you modify a file in a Vite project, the update "bubbles up" the module graph. If no module in that chain knows how to handle the update, Vite triggers a full page reload to ensure the application state is consistent. For complex components or stateful modules—such as WebGL canvases, socket connections, or complex timers—a full reload destroys the current application state and slows down the development loop.
The solution is to establish an HMR Boundary using the import.meta.hot API. This allows you to tell Vite: "I can handle the update for this module; do not reload the rest of the page."
How the HMR Mechanism Works
Vite utilizes native ES Modules (ESM) in the browser. Instead of rebuilding a massive bundle, Vite's development server maintains a WebSocket connection to the browser. When a file changes, the server sends a notification via WebSocket, and the browser requests only the modified module and its immediate dependencies. This process is called Hot Module Replacement (HMR).
The import.meta.hot object is a development-only API provided by Vite. It allows you to define custom logic for cleaning up the old version of a module before the new version is executed.
Worked Example: Managing a Persistent Timer
Consider a module that starts a setInterval. Without custom HMR logic, every time you save the file, a new interval would start while the old one continues to run in the background, leading to memory leaks and erratic behavior.
// timer.js
let timerId = null;
export function startTimer(callback) {
if (timerId) return;
timerId = setInterval(() => {
console.log('Tick...');
callback();
}, 1000);
}
export function stopTimer() {
clearInterval(timerId);
timerId = null;
}
// HMR Logic
if (import.meta.hot) {
// 1. Handle cleanup of the current module before it is replaced
import.meta.hot.dispose(() => {
console.log('Cleaning up timer before update...');
stopTimer();
});
// 2. Accept the update to stop the bubble from reaching the root
import.meta.hot.accept((newModule) => {
if (newModule) {
console.log('Timer module updated successfully');
}
});
}
Implementation Details
import.meta.hot.dispose(): This registers a callback that runs immediately before the module is invalidated. Use this to clear timers, remove event listeners, or close WebSocket connections.import.meta.hot.accept(): This marks the module as an HMR boundary. The callback receives the updated module, allowing you to re-initialize state or pass data from the old version to the new one.
Common Failures and Limitations
The State Desynchronization Trap
A common mistake is calling accept() without implementing dispose(). If your module modifies the DOM or adds global event listeners, these will persist across HMR updates. Over time, your application will accumulate "ghost" listeners, causing performance degradation or duplicate event triggers.
Circular Dependencies
Vite's HMR relies on a directed acyclic graph (DAG) of module dependencies. If Module A imports Module B, and Module B imports Module A, the HMR chain can break. When this happens, Vite cannot determine the safe order of updates and will default to a full page reload, regardless of whether you have used import.meta.hot.accept().
Framework-Specific Overrides
If you are using @vitejs/plugin-vue or @vitejs/plugin-react, these plugins implement their own HMR boundaries automatically for .vue or .jsx files. You only need import.meta.hot for vanilla JavaScript/TypeScript logic or custom non-component modules.
Verifying the HMR Boundary
To verify that your custom HMR logic is working and not triggering a full reload, follow these steps:
- Open your browser's Developer Tools and go to the Network tab.
- Filter for
WS(WebSockets) to ensure the Vite HMR connection is active. - Modify a string or a logic block inside the module using
import.meta.hot.accept()and save the file. - Observe the Console: You should see your "Cleaning up..." and "Updated successfully" logs.
- Check the Network tab: You should see a new request for the modified
.jsfile, but the page should not flash or reset its scroll position (which would indicate a full reload).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.