Architecting Fast Updates: Implementing Custom HMR Logic in Vite
Learn how to implement custom Hot Module Replacement (HMR) in Vite to prevent full page reloads and preserve application state during development.
18 Aug 2026, 00:36 UTC

The Problem: Full Page Reloads During Development
The primary friction in frontend development is the loss of application state during a code change. When a developer saves a file, a full browser refresh clears the current UI state, resets form inputs, and destroys the current navigation context. While Vite provides automatic Hot Module Replacement (HMR) for many frameworks, vanilla JavaScript projects or custom internal libraries often default to a full page reload because the module doesn't know how to "accept" its own update.
The goal is to implement a targeted update mechanism where only the modified module is replaced in the browser's memory, preserving the rest of the application state.
Requirements for State-Preserving Updates
To move from a full reload to a surgical update, the architecture must satisfy three requirements:
- Invalidation Signal: The server must notify the client exactly which module changed via a WebSocket.
- Dependency Tracking: The browser must identify if the changed module is "accepted" by itself or by one of its parents.
- Cleanup Logic: The old version of the module must be disposed of to prevent memory leaks or duplicate event listeners.
The Smallest Suitable Design: The HMR Accept Pattern
Vite exposes the HMR API through import.meta.hot. The most efficient design for a custom module is to make it self-accepting. This prevents the update signal from bubbling up to the root of the application, which would otherwise trigger a full reload.
Implementation Example
In a vanilla JavaScript project (Vite v5+), use the following pattern to handle a module that updates the DOM without refreshing the page:
// counter.js
export function render() {
document.getElementById('app').innerHTML = `<h1>Count: ${window.count || 0}</h1>`;
}
// HMR Logic
if (import.meta.hot) {
// 1. Handle the update
import.meta.hot.accept((newModule) => {
if (newModule) {
// Call the updated render function
newModule.render();
}
});
// 2. Cleanup old state/listeners
import.meta.hot.dispose(() => {
console.log('Cleaning up old module version...');
});
}
Configuration and Permissions
This logic runs in the browser environment. No special server-side permissions are required beyond the standard vite dev command. The import.meta.hot object is only injected by the Vite development server; it is stripped out during the production build (vite build), ensuring no development overhead reaches the end user.
Trust and Data Boundaries
The HMR boundary exists between the Vite Dev Server (Node.js) and the Browser Client. The server trusts the file system for change detection but does not trust the browser to manage the module graph. The server sends an update event via WebSocket containing the URL of the changed file. The browser client then requests that specific module via a native ES Module (ESM) import, appending a timestamp (e.g., ?t=123456) to bypass the browser's HTTP cache.
Operational Checks and Verification
To verify that HMR is working as intended rather than triggering a full reload, perform these three checks:
| Check | Action | Expected Result |
|---|---|---|
| Network Tab | Save a file change | A single request for the modified file (with a ?t= query string), not a full page reload. |
| Console Log | Check DevTools | The dispose callback log should appear immediately before the accept callback. |
| State Persistence | Change a UI string in code | The text updates in the browser, but existing input values or JS variables remain unchanged. |
Failure Modes and Design Shifts
Circular Dependencies
If Module A imports Module B, and Module B imports Module A, the HMR signal may enter an infinite loop or fail to find a "boundary" that accepts the update. In this scenario, the design must shift: move the shared state to a third, independent Module C that both A and B import.
Large Dependency Chains
If a low-level utility file is changed, and 50 other modules depend on it, Vite must invalidate all 50 modules. This can lead to a "waterfall" of HTTP requests. If network congestion occurs, the design should shift toward Module Federation or splitting the utility into smaller, more granular files to reduce the invalidation blast radius.
State Loss in Frameworks
While the HMR API handles the code update, it does not automatically preserve internal variable state. If you need to preserve a counter value across updates, you must move that state to a global object (like window) or a persistent store outside the module being replaced.
Rollback Procedure
Because HMR changes the state of the running browser application, the only way to "rollback" to a known clean state is a hard browser refresh (Cmd+R / Ctrl+R). This clears the modified module graph and re-initializes the application from the current disk state.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.