Hot Module Replacement (HMR): How It Works and Why It Matters


Ever changed a button color, tweaked a UI element, or updated a function—only to refresh the page and lose everything? Losing form inputs, app state, or debugging progress due to a full reload is frustrating.
Hot Module Replacement (HMR) solves this problem by enabling live updates of modules (JavaScript, CSS, etc.) in a running application without a full page refresh, preserving state and instantly reflecting code changes.
Why is HMR Important?
Faster Feedback Loop: No need to reload the entire app after every change.
Preserve Application State: Maintains the current state of the app during development.
Improved Productivity: Reduces development time and frustration.
The Problem HMR Solves
Before HMR, developers relied on Live Reload, which refreshed the entire page on every change. This reset the app’s state, making it tedious to debug interactions (e.g., multi-step forms, animations, and API-dependent UI changes).
HMR vs. Live Reload
Feature | HMR (Hot Module Replacement) | Live Reload |
State Preservation | Yes | No (State resets) |
Update Speed | Faster (updates only modified code) | Slower (full page reload) |
Implementation Complexity | Requires bundler support | Simple (just refreshes the page) |
How HMR Works
HMR is typically implemented by bundlers like Webpack, Vite, and Parcel. The process involves multiple components and steps to efficiently replace modules without requiring a full reload.
Core Components of HMR
Module Graph:
A dependency map of all files and their relationships
Used to determine which modules need recompilation when a file changes.
Example: Updating
styles.css
triggers updates in all modules that import it.
HMR Server:
Runs alongside the development server (e.g.,
webpack-dev-server
,vite dev
).Watches files for changes using operating system’s file system APIs (e.g.,
inotify
on Linux,FSWatch
on macOS).Rebuilds only affected modules (via the module graph).
Sends updates to the client via the communication channel.
HMR Runtime:
Injected into the browser by the bundler (e.g., as part of the app’s bundled code).
Listens for updates from the HMR server.
Applies new code dynamically (e.g., replacing modules, injecting CSS).
Handles errors and falls back to full reloads if needed.
Communication Channel:
Enables real-time server-client messaging.
Uses WebSockets (not HTTP) for instant, bidirectional communication.
The HMR Update Process
Let’s go through the HMR update process using a simple react app as an example
1. Button.jsx
(Child Component)
import React, { useState } from 'react';
import './styles.css';
const Button = () => {
const [count, setCount] = useState(0);
return (
<button
className="my-button"
onClick={() => setCount(c => c + 1)}
>
Clicks: {count}
</button>
);
};
export default Button;
2. App.jsx
(Parent Component)
import React from 'react';
import Button from './Button';
const App = () => {
return (
<div className="app">
<h1>HMR Demo</h1>
<Button />
</div>
);
};
export default App;
3. styles.css
.my-button {
padding: 12px;
background: blue;
color: white;
}
Currently, The counter shows Clicks: 0
.
Now, let’s walkthrough the HMR process as we modify the button’s text from "Clicks: {count}"
to "Total Clicks: {count}"
and save Button.jsx
.
Step 1: Change Detection
File Monitoring:
The HMR server detects theButton.jsx
change using OS-level watchers.Dependency Resolution:
The module graph (a dependency map) identifies:Direct dependency:
styles.css
(imported byButton.jsx
).Parent relationship:
App.jsx
rendersButton.jsx
.
Change Validation:
A content hash (e.g., SHA-1) verifies the change is significant (not whitespace) before proceeding with the update.
Step 2: Partial Rebuilding
The goal here is to rebuild only affected modules for efficiency.
Partial Compilation:
Button.jsx
andstyles.css
are recompiled.App.jsx
is not recompiled—it only rendersButton
and isn’t tied to its internal logic.
Code Transformation:
Transpilers transform the code into browser-compatible formats, such as transpiling TypeScript to JavaScript or SCSS to CSS.
Preparing the Update Package:
The bundler creates an update bundle containing:{ "id": "src/Button.jsx", // Changed module ID "code": "/* Transpiled JS */", // New component code "deps": ["src/styles.css"] // Affected dependencies }
Step 3: Sending Updates
The HMR server then sends a WebSocket message to the client.
This message contains metadata, including the updated module's ID and new code.
{ "type": "hot-update", "moduleId": "src/Button.jsx", "code": "function Button() { ... }", // New transpiled code "css": ".my-button { ... }" // Updated CSS }
Step 4: Applying Updates
Once the HMR runtime receives the updated module, it evaluates how to apply the changes efficiently without requiring a full page reload.
Handling Different Types of Updates
CSS Updates
The updated styles are injected into the DOM dynamically.
No page refresh occurs, preventing flickers or input losses.
JavaScript Module Updates
How HMR updates JavaScript modules depends on the bundler:
Webpack: Webpack does not automatically replace modules during HMR. Instead, modules must explicitly opt-in using
module.hotaccept()
for versions before Webpack 5 orimport.meta.webpackHot.accept()
for Webpack 5.Example: Enabling HMR for a JavaScript Module in Webpack
//counter.js let count = 0; export function increment() { count++; console.log(`Count: ${count}`); } // Accept HMR updates for this module if (import.meta.webpackHot) { import.meta.webpackHot.accept(); }
How It Works:
The
import.meta.webpackHot.accept()
method tells Webpack that this module can accept updates without requiring a full page reload.When
counter.js
is modified, Webpack will replace it in memory and apply the updates dynamically.
Vite & Parcel: Unlike Webpack, Vite and Parcel automatically apply updates whenever they detect that a module can be safely replaced. They do not require explicit
import.meta.webpackHot.accept()
.
Regardless of the bundler, if a module cannot self-update, HMR will:
Propagate the update up the dependency graph, searching for a parent module that can handle it.
If no parent can accept the update, a full page reload is triggered to ensure the application remains in a consistent state.
React Component Updates
For React components, HMR is seamlessly managed by React Fast Refresh, which ensures:
Component Compatibility Check
If the component’s structure remains compatible (e.g., UI or text changes), HMR updates it in place.
If the structure changes significantly (e.g., new hooks or converting from a class to a function), the component remounts instead.
State Preservation
- If the structure remains the same, React preserves state (e.g.,
useState
values stay intact).
- If the structure remains the same, React preserves state (e.g.,
Efficient DOM Updates
- React’s Virtual DOM ensures minimal re-renders, updating only what changed.
In our example:
If before saving
Button.jsx
, the counter displays:Clicks: 3
After modifying the text to
Total Clicks: {count}
and saving,The text updates without a page reload.
The counter remains at 3 because the component state is preserved
Step 5: Error Handling and Recovery
If an error occurs while applying updates, the HMR runtime attempts to recover by retrying the update.
If errors persist, the application falls back to a full reload to ensure a consistent state.
HMR includes safeguards to prevent crashes:
Automatic Retries: Temporary issues (e.g., network errors) trigger retries.
Fallback to Reload: If HMR cannot apply the update cleanly, a full reload is triggered.
Clear Error Messages: The browser’s console displays warnings to help debug issues.
Conclusion
HMR significantly enhances the development process by enabling real-time updates without requiring a full page refresh. It improves development speed, preserves state, and boosts productivity. Understanding its internal mechanisms allows developers to fully utilise HMR for a more efficient workflow. By understanding its inner workings and best practices, developers can fully leverage HMR’s capabilities for a smoother workflow.
Subscribe to my newsletter
Read articles from MyCodingNotebook directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

MyCodingNotebook
MyCodingNotebook
MyCodingNotebook is every developer’s online notebook—a space to explore coding concepts, share insights, and learn best practices. Whether you're just starting or an experienced developer, this blog serves as a go-to resource for coding best practices, real-world solutions, and continuous learning. Covering topics from foundational programming principles to advanced software engineering techniques, MyCodingNotebook helps you navigate your coding journey with clarity and confidence. Stay inspired, keep learning, and continue growing in your coding journey.