HMR(Hot Module Replacement)
A Note to Our Readers
While maintaining fidelity to the source material, this translation incorporates explanatory content and localized expressions, informed by the translator's domain expertise. These thoughtful additions aim to enhance readers' comprehension of the original text's core messages. For any inquiries about the content, we welcome you to engage in the discussion section or consult the source text for reference.
If you're using Vite
to build your project, you're likely also using Hot Module Replacement (HMR
). HMR
allows you to update your code without refreshing the page, such as editing component markup or adjusting styles, with changes immediately reflected in the browser. This helps speed up code interaction and improve developer experience.
While HMR
is also a feature of other bundlers like Webpack
and Parcel
, in this article we'll dive deep into how it works in Vite
. Typically, other bundlers should operate similarly.
First, it's important to note that HMR
is not simple, and some topics may take some time to digest, but I hope I've piqued your interest! On this page, you'll learn:
What Conditions Are Needed for Module Replacement
Essentially, HMR is the process of dynamically replacing modules while the application is running. Most bundlers use ECMAScript
modules (ESM) as modules because it's easier to analyze module imports and exports, which helps understand how replacing one module will affect other related modules.
A module typically has access to HMR
lifecycle APIs, which are used to handle operations when old modules are discarded and new modules are in place. In Vite
, you have the following APIs available:
- import.meta.hot.accept()
- import.meta.hot.dispose()
- import.meta.hot.prune()
- import.meta.hot.invalidate()
Overall, they work like this:
It's important to note that you need to use these APIs for HMR
to work properly. For example, Vite
uses these APIs out of the box to handle CSS
files, but for other files (like Vue
and Svelte
), you can use a Vite
plugin to call these HMR
APIs. Or handle them manually as needed. Otherwise, by default, updates to files will cause the entire page to reload.
Beyond that, let's dive into how these APIs work!
import.meta.hot.accept()
When you use import.meta.hot.accept()
and attach a callback, the callback will be responsible for replacing the old module with the new module. A module using this API is also called an accepted module
.
Accepted modules
create an HMR boundary
. The HMR boundary
includes the module itself and all recursively imported modules (all parent modules that depend on the current module and all their ancestor modules). The accepted module is also the "root" of the HMR boundary
, as this boundary is typically a graph structure.
Accepted modules
can also be narrowed down to "self-accepting modules" based on how the HMR
callback is attached. import.meta.hot.accept has two function signatures:
- import.meta.hot.accept(cb: Function) - Accept changes from itself
- import.meta.hot.accept(deps: string | string[], cb: Function) - Accept changes from imported modules
If using the first signature, the current module can be called a self-accepting module
. This distinction is important for HMR propagation
, which we'll discuss later.
Here's how they're used:
export let data = [1, 2, 3];
if (import.meta.hot) {
import.meta.hot.accept(newModule => {
// Replace the old value with the new one
data = newModule.data;
});
}
import { value } from './stuff.js';
document.querySelector('#value').textContent = value;
if (import.meta.hot) {
import.meta.hot.accept(['./stuff.js'], ([newModule]) => {
// Re-render with the new value
document.querySelector('#value').textContent = newModule.value;
});
}
import.meta.hot.dispose()
When an accepted module or a module accepted by other modules is being replaced with a new module, or is being removed, we can use import.meta.hot.dispose()
for cleanup. This allows us to clean up any side effects created by the old module, such as removing event listeners, clearing timers, or resetting state.
Here's an example of the API:
globalThis.__my_lib_data__ = {};
if (import.meta.hot) {
import.meta.hot.dispose(() => {
// Reset global state
globalThis.__my_lib_data__ = {};
});
}
import.meta.hot.prune()
When a module needs to be completely removed from runtime, such as when a file is deleted, we can use import.meta.hot.prune()
to perform final cleanup. This is similar to import.meta.hot.dispose()
, but is only called once when the module is removed.
Internally, Vite
prunes modules at different stages through import analysis (analyzing module imports), because the only time we can know a module is no longer in use is when it's no longer imported (depended on) by any other module.
Here's an example of Vite
using the CSS HMR API:
// Import utilities to update/remove style tags in the HTML
import { removeStyle, updateStyle } from '/@vite/client';
updateStyle('/src/style.css', 'body { color: red; }');
if (import.meta.hot) {
// Empty accept callback is we want to accept, but we don't have to do anything.
// `updateStyle` will automatically get rid of the old style tag.
import.meta.hot.accept();
// Remove style when the module is no longer used
import.meta.hot.prune(() => {
removeStyle('/src/style.css');
});
}
import.meta.hot.invalidate()
Unlike the above APIs, import.meta.hot.invalidate()
is an operation
rather than a lifecycle hook
. You typically use it inside import.meta.hot.accept
when you might realize at runtime that a module cannot be safely updated and needs to bail out.
When this method is called, the Vite server
will be notified that the module is invalid, as if it had been updated. HMR propagation
will execute again to determine if any of its importers can recursively accept this change.
Here's an example of the API:
export const data = [1, 2, 3];
if (import.meta.hot) {
import.meta.hot.accept(newModule => {
// If the `data` export is deleted or renamed
if (!(data in newModule)) {
// Bail out and invalidate the module
import.meta.hot.invalidate();
}
});
}
Other HMR APIs
The Vite HMR documentation covers more APIs. However, they are not key to understanding how HMR works, so we'll skip them for now, but we'll return to these APIs when discussing the HMR client later.
If you're interested in how they can be useful in certain situations, please take a quick read of the documentation!
From the Beginning
From the above content, we've learned about the HMR APIs
and how these APIs allow developers to replace and manage modified modules. But there's still a missing piece: how do we know when to replace a module? HMR typically occurs after editing a file, but what happens after that?
At first glance, the situation looks something like this:
Let's explain the specific process in detail.
Editing a file
HMR
begins when you edit a file and save it. A file system watcher like chokidar
detects the change and passes the edited file path to the next step.
Processing edited modules
The Vite
dev server receives the edited file path information. With these edited file paths, it finds the corresponding module information using the module dependency graph. It's important to note that "file"
and "module"
are two different concepts - a file may correspond to one or multiple modules. For example, a Vue file
can be compiled into a JavaScript module
and related CSS modules
.
These modules are then passed to the handleHotUpdate()
hook of Vite
plugins for further processing. Plugins can choose to filter or extend the array of modules needed. The final modules will be passed to the next step.
Here are some plugin examples:
// Example: filter out array of modules
function vuePlugin() {
return {
name: 'vue',
async handleHotUpdate(ctx) {
if (ctx.file.endsWith('.vue')) {
const oldContent = cache.get(ctx.file);
const newContent = await ctx.read();
// If only the style has changed when editing the file, we can filter
// out the JS module and only trigger the CSS module for HMR.
if (isOnlyStyleChanged(oldContent, newContent)) {
return ctx.modules.filter(m => m.url.endsWith('.css'));
}
}
}
};
}
// Example: extending array of modules
function globalCssPlugin() {
return {
name: 'global-css',
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.css')) {
// If a CSS file is edited, we also trigger HMR for this special
// `virtual:global-css` module that needs to be re-transformed.
const mod = ctx.server.moduleGraph.getModuleById(
'virtual:global-css'
);
if (mod) {
return ctx.modules.concat(mod);
}
}
}
};
}
Module invalidation
Before HMR propagation
, we eagerly recursively invalidate the final array of updated modules and their importers. Each module's transformed code will be removed, and a timestamp will be appended. This timestamp will be used to get the new module on the client side the next time it's requested.
HMR propagation
To update the final array of modules now, HMR propagation
will be executed. This is where the "magic" happens, and it's often the source of confusion that causes HMR
to not work as expected.
HMR propagation
is essentially about starting with the module to be updated to retrieve the required HMR boundary
. If all modules to be updated are within a boundary, the Vite
dev server will notify the HMR client
that the accepted module should execute HMR
. If some are not within the boundary, then a full page reload will be triggered.
To better understand how it works, let's look at this example by case:
Scenario 1: If updated
stuff.js
, propagation will recursively find its parent dependency modules and all their ancestor dependency modules to find an accepted module. In this case, we'll find thatapp.jsx
is an accepted module. But before ending propagation, we need to determine ifapp.jsx
can accept changes fromstuff.js
. This depends on howimport.meta.hot.accept()
is called.- Scenario 1(a): If
app.jsx
is self-accepting, or it accepts changes fromstuff.js
, we can stop continuing up the propagation, because no other module depends onstuff.js
module. Then theHMR client
will notifyapp.jsx
to executeHMR
. - Scenario 1(b): If
app.jsx
doesn't acceptstuff.js
module changes, then we'll continue up the propagation to find an accepted module. But since there's no other accepted module, we'll reach "root"index.html
file. This will trigger a full page reload.
- Scenario 1(a): If
Scenario 2: If updated
main.js
orother.js
, then propagation will again recursively find its parent dependency modules and all their ancestor dependency modules. However, we'll find no accepted module, and we'll reach "root" index.html file. Therefore, a full page reload will be triggered.Scenario 3: If updated
app.jsx
, then we immediately find it as an accepted module. However, some modules may not be able to update themselves. We can determine if an accepted module is self-accepting to determine if an accepted module can update itself.- Scenario 3(a): If
app.jsx
is self-accepting, we can stop here and let theHMR client
notify this module to executeHMR
. - Scenario 3(b): If
app.jsx
is not self-accepting, we'll continue up the propagation to find an accepted module. But since we didn't find any, and we'll reach "root" index.html file, this will trigger a full page reload.
- Scenario 3(a): If
Scenario 4: If updated
utils.js
, propagation will again recursively find its parent dependency modules and all their ancestor dependency modules. First, we'll findapp.jsx
as an accepted module, and we'll stop propagation there (assuming it meets Scenario 1(a)). Then we'll also recursively findother.js
and its parent dependency modules and all their ancestor dependency modules, but we'll find no accepted module, and we'll reach "root" index.html file. Ifat least one
module is not accepted, then a full page reload will be triggered.
If you want to learn about more advanced scenarios involving multiple HMR boundaries, please click on the foldable part below:
Details
Switching to advanced scenarios, let's look at a different example involving three .jsx files in 3 HMR boundaries:
Scenario 5: If updated
stuff.js
, propagation will recursively find its importers to find an accepted module. We'll find thatcomp.jsx
is an accepted module, and we'll handle it the same way as Scenario 1. To reiterate:- Scenario 5(a): If
comp.jsx
is self-accepting, orcomp.jsx
accepts changes fromstuff.js
, then we can stop propagation. Then theHMR client
will notifycomp.jsx
to executeHMR
. - Scenario 5(b): If
comp.jsx
doesn't accept this change, we'll continue up the propagation to find an accepted module. We'll find thatapp.jsx
is an accepted module, and we'll handle it the same way as Scenario 5! Until we find a module that can acceptstuff.js
changes, if there's a branch that retrieves "root" index.html, then we need to perform a full page load.
- Scenario 5(a): If
Scenario 6: If updated
bar.js
, propagation will recursively find its importers and find thatcomp.jsx
andalert.jsx
are accepted modules. We'll also handle these two modules the same way as Scenario 5. Assuming the best case is that bothcomp.jsx
andalert.jsx
modules are Scenario 5(a), that is, both receivebar.js
updates, then theHMR client
will notifycomp.jsx
andalert.jsx
two modules to executeHMR
together.Scenario 7: If updated
utils.js
, propagation will again recursively find its importers(importers) and find all direct importerscomp.jsx
,alert.jsx
, andapp.jsx
, and all are accepted modules. We'll also handle these three modules(comp.jsx
,alert.jsx
, andapp.jsx
) the same way as Scenario 5. Assuming the best case is all accepted modules are Scenario 5(a), even ifcomp.jsx
is also aHMR boundary
part ofapp.jsx
, theHMR client
will notify them three to executeHMR
. (Future, Vite may detect this and only notifyapp.jsx
andalert.jsx
, but this is largely an implementation detail!)Scenario 8: If updated
comp.jsx
, we immediately find it as an accepted module. Like Scenario 3, we need to first check ifcomp.jsx
is self-accepting.- Scenario 8(a): If
comp.jsx
is self-accepting, then we stop propagation and let theHMR client
notifycomp.jsx
to executeHMR
. - Scenario 8(b): If
comp.jsx
is not self-accepting, then we can handle it the same way as Scenario 5(b).
- Scenario 8(a): If
Besides the above content, there are many other edge cases not covered here because they're a bit complex, including circular imports, partial accepted modules, only CSS importers, etc. However, as you become more familiar with the whole process, you can revisit them!
Finally, the result of HMR propagation
is to determine whether to perform a full page load or should execute HMR
in the client.
If it's determined that a full page reload is needed, then a message will be sent to the HMR client
and told to reload the page.
If it's determined that a module can be hot updated, then in the HMR propagation period all accepted modules that meet the condition are synthesized array sent to the HMR client
, the client will trigger the HMR APIs
we discussed above, thus executing HMR
.
How Does This HMR Client Work?
In a Vite
application, you may notice that Vite
adds a special script to the HTML
to request /@vite/client
script. This script contains the logic for handling the HMR client
.
The HMR client
is responsible for the following:
- Establish a
WebSocket
connection with theVite
dev server. - Listen for
HMR payloads
from theVite
server. - Provide and trigger
HMR APIs
in the runtime phase. - Send any events back to the
Vite
dev server.
From a broader perspective, the HMR client
helps establish the connection between the Vite
dev server and the HMR APIs
. Let's see how this connection works.
Client initialization
Before the HMR client
can receive any messages from the Vite
dev server, it needs to first establish a connection through WebSockets
. Here's an example of setting up a WebSocket
connection for further processing the results of HMR propagation
from the Vite
dev server:
// /@vite/client (URL)
const ws = new WebSocket('ws://localhost:5173');
ws.addEventListener('message', ({ data }) => {
const payload = JSON.parse(data);
switch (payload.type) {
case '...':
// Handle payloads...
}
});
// Send any events to the Vite dev server
ws.send('...');
In the next section, we'll discuss payload processing in more detail.
Besides, the HMR client
also initializes some state for handling HMR
and exports several APIs
, such as createHotContext()
, for modules using HMR APIs
. For example:
// Injected by Vite's import-analysis plugin
import { createHotContext } from '/@vite/client';
import.meta.hot = createHotContext('/src/app.jsx');
export default function App() {
return <div>Hello World</div>;
}
// Injected by `@vitejs/plugin-react`
if (import.meta.hot) {
// ...
}
The URL
string (also called "owner path") passed to createHotContext()
helps determine which module can accept changes. Internally, createHotContext
will assign registered HMR callbacks
to a singleton mapping, which will contain all mappings from owner path to accept callbacks, dispose callback, and prune callback. We'll explain this in more detail later!
This is basically how modules
interact with the HMR client
and execute HMR changes
.
Handling payloads from the server
After establishing a WebSocket
connection, we can start handling valid callbacks from the Vite dev server
.
// /@vite/client (URL)
ws.addEventListener('message', ({ data }) => {
const payload = JSON.parse(data);
switch (payload.type) {
case 'full-reload': {
location.reload();
break;
}
case 'update': {
const updates = payload.updates;
// => { type: string, path: string, acceptedPath: string, timestamp: number }[]
for (const update of updates) {
handleUpdate(update);
}
break;
}
case 'prune': {
handlePrune(payload.paths);
break;
}
// Handle other payload types...
}
});
The above example handles the results of HMR propagation
, determining whether to trigger full page reload or HMR update, depending on the full-reload
and update
callback parameters to distinguish. It also handles pruning when the module is no longer used.
Callback parameters also have other properties, not all of which are for HMR
but briefly mentioned:
- connected: Sent when a WebSocket connection is established.
- error: Sent when an error occurs on the server side,
Vite
can display the specific error content in the browser console. - custom: Sent by
Vite
plugins to notify client any events. It's useful for interconnection between client and server.
Continuing forward, let's see how HMR update
works in practice.
HMR update
Each HMR boundary
found in HMR propagation
usually corresponds to an HMR update
. In Vite
, updates take this signature:
interface Update {
// The type of update
type: 'js-update' | 'css-update';
// The URL path of the accepted module (HMR boundary root)
path: string;
// The URL path that is accepted (usually the same as above)
// (We'll talk about this later)
acceptedPath: string;
// The timestamp when the update happened
timestamp: number;
}
Different HMR
implementations can freely reshape the above update signature. In Vite
, Update
is divided into js update
or css update
.
css update
is specially handled as simply swapping link
tags in the HTML
.
For js update
, we need to find the corresponding new module and call its import.meta.hot.accept()
callback function, through the callback function to apply HMR
to itself. Since we've registered the path as the first parameter in createHotContext()
(i.e., determining the mapping between path and execution function at page execution time), then we can easily find the matching module through the updated path
and get the latest module information according to the updated timestamp, and pass the new module to the import.meta.hot.accept()
callback function. The implementation logic is simplified as follows:
// /@vite/client (URL)
// Map populated by `createHotContext()`
const ownerPathToAcceptCallbacks = new Map<string, Function[]>();
async function handleUpdate(update: Update) {
const acceptCbs = ownerPathToAcceptCallbacks.get(update.path);
const newModule = await import(
`${update.acceptedPath}?t=${update.timestamp}`
);
for (const cb of acceptCbs) {
cb(newModule);
}
}
However, it's important to note that import.meta.hot.accept()
has two function signatures?
import.meta.hot.accept(cb: Function)
import.meta.hot.accept(deps: string | string[], cb: Function)
The above implementation logic only applies to the first function signature (i.e., self-accepting module
), but not to the second. The callback of the second function signature is only called when dependency updates occur. We can bind each callback to a set of dependencies, like this:
// URL: /src/app.jsx
import { add } from './utils.js';
import { value } from './stuff.js';
if (import.meta.hot) {
import.meta.hot.accept(/** ... */);
// { deps: ['/src/app.jsx'], fn: ... }
import.meta.hot.accept('./utils.js' /** ... */);
// { deps: ['/src/utils.js'], fn: ... }
import.meta.hot.accept(['./stuff.js'] /** ... */);
// { deps: ['/src/stuff.js'], fn: ... }
}
Then we can use acceptedPath
to match dependencies and trigger the correct callback function. For example, if updated stuff.js
, then acceptedPath
will be /src/stuff.js
, and path will be /src/app.jsx
. We can adjust the HMR
processing program as follows:
// Map populated by `createHotContext()`
const ownerPathToAcceptCallbacks = new Map<
string,
{ deps: string[]; fn: Function }[]
>()
async function handleUpdate(update: Update) {
const acceptCbs = ownerPathToAcceptCallbacks.get(update.path)
const newModule = await import(`${update.acceptedPath}?t=${update.timestamp}`)
for (const cb of acceptCbs) {
// Make sure to only execute callbacks that can handle `acceptedPath`
if (cb.deps.some((deps) => deps.includes(update.acceptedPath))) {
cb.fn(newModule)
}
}
}
But it's not over. Before importing the new module, we also need to ensure old modules are handled correctly through import.meta.hot.dispose()
.
// Maps populated by `createHotContext()`
const ownerPathToAcceptCallbacks = new Map<
string,
{ deps: string[]; fn: Function }[]
>()
const ownerPathToDisposeCallback = new Map<string, Function>()
async function handleUpdate(update: Update) {
const acceptCbs = ownerPathToAcceptCallbacks.get(update.path)
// Call the dispose callback if there's any
ownerPathToDisposeCallbacks.get(update.path)?.()
const newModule = await import(`${update.acceptedPath}?t=${update.timestamp}`)
for (const cb of acceptCbs) {
// Make sure to only execute callbacks that can handle `acceptedPath`
if (cb.deps.some((deps) => deps.includes(update.acceptedPath))) {
cb.fn(newModule)
}
}
}
Then we basically have most of what the HMR client
needs to do! As further practice, you can also try to implement error handling, empty owner check, queue and parallel update for predictability, etc., which will make the final form more robust.
HMR pruning
As discussed in import.meta.hot.prune()
, Vite
handles HMR pruning
internally in the "import analysis
" stage. When a module is no longer imported by any other module, the Vite
dev server will send a { type: 'prune', paths: string[] }
object to the HMR client
, and the HMR client
will independently prune these invalid modules at runtime.
// /@vite/client (URL)
// Maps populated by `createHotContext()`
const ownerPathToDisposeCallback = new Map<string, Function>();
const ownerPathToPruneCallback = new Map<string, Function>();
function handlePrune(paths: string[]) {
for (const p of paths) {
ownerPathToDisposeCallbacks.get(p)?.();
ownerPathToPruneCallback.get(p)?.();
}
}
HMR invalidation
Unlike other HMR APIs
, import.meta.hot.invalidate()
is an API
that can be called inside import.meta.hot.accept()
when you might realize at runtime that a module cannot be safely updated and needs to bail out. In /@vite/client
, simply sending a WebSocket
message to the Vite
dev server is enough:
// /@vite/client (URL)
// `ownerPath` comes from `createHotContext()`
function handleInvalidate(ownerPath: string) {
ws.send(
JSON.stringify({
type: 'custom',
event: 'vite:invalidate',
data: { path: ownerPath }
})
);
}
When the Vite
server receives this request, it will again execute HMR propagation
, starting from its importers, and send the result (**full reload**
or execute HMR update) back to the HMR client
.
HMR events
Although not required for HMR
, the HMR client
can also emit events at runtime when receiving specific payloads. import.meta.hot.on
and import.meta.hot.off
can be used to listen and unlisten to these events.
if (import.meta.hot) {
import.meta.hot.on('vite:invalidate', () => {
// ...
});
}
Emitting and tracking these events is very similar to how we handle the above HMR callbacks
. Taking HMR invalidation
code as an example:
const eventNameToCallbacks = new Map<string, Set<Function>>();
// `ownerPath` comes from `createHotContext()`
function handleInvalidate(ownerPath: string) {
eventNameToCallbacks.get('vite:invalidate')?.forEach((cb) => cb());
ws.send(
JSON.stringify({
type: 'custom',
event: 'vite:invalidate',
data: { path: ownerPath }
})
);
}
HMR data
Finally, the HMR client
also provides a way to store data to be shared between HMR APIs
using import.meta.hot.data
. This data can be seen passed to the HMR callback functions
of import.meta.hot.dispose()
and import.meta.hot.prune()
.
Storing data is similar to how we track HMR callbacks
. Taking the HMR pruning
code as an example:
// Maps populated by `createHotContext()`
const ownerPathToDisposeCallback = new Map<string, Function>();
const ownerPathToPruneCallback = new Map<string, Function>();
const ownerPathToData = new Map<string, Record<string, any>>();
function handlePrune(paths: string[]) {
for (const p of paths) {
const data = ownerPathToData.get(p);
ownerPathToDisposeCallbacks.get(p)?.(data);
ownerPathToPruneCallback.get(p)?.(data);
}
}
Summary
That's all about HMR
! In short, we learned:
- How to use
HMR APIs
to handle file changes. - How file editing causes the
Vite dev server
to sendHMR updates
to theHMR client
. - How the
HMR client
processesHMR payloads
and triggers the correctHMR APIs
.
Before we end, please check out the FAQ below if you still have questions about how certain things work.
FAQ
Where can I find the source code for
Vite
'sHMR
implementation?- vite/src/client/client.ts - Implementation source code for /@vite/client.
- vite/src/shared/hmr.ts - The HMR client implementation used by /@vite/client. Also abstracted away for the HMR client in SSR. (See HMRClient)
- vite/src/node/server/hmr.ts - Handle HMR propagation. (See handleHMRUpdate)
Are there any
HMR examples
to learn from?HMR
is typically implemented byJS frameworks
that introduce the concept of "components", where each component can isolate its state and reinitialize itself. Therefore, you can look at how frameworks likeReact
,Vue
, andSvelte
implement them:React:Fast Refresh and @vitejs/plugin-react
Vue:@vue/runtime-core and @vitejs/plugin-vue
Svelte:svelte-hmr and @vitejs/plugin-svelte
How does
Vite
's implementation differ fromWebpack
and other tools?I haven't delved deep into
Webpack
's implementation, only understandingWebpack
'sHMR
principles from theWebpack
documentation and thisNativeScript
article. As far as I know, a common difference is thatWebpack
handlesHMR propagation
on the client side rather than the server side.This difference has a benefit:
HMR APIs
can be used more dynamically. In contrast,Vite
needs to statically parse theHMR APIs
used in modules on the server side to determine if a module has calledimport.meta.hot.accept()
. However, handlingHMR propagation
on the client side can be complex because some important information (such as importers, exports, ids, etc.) only exists on the server. Due to the need for this important information, it might require restructuring to get serialized module information on the client side and keep it synchronized with the server, which could be complex.How does HMR work in server-side rendering?
At the time of writing this article (
Vite 5.0
),HMR
inSSR
is not yet supported, but will be released as an experimental feature inVite 5.1
. Even withoutHMR
inSSR
, forJS
frameworks likeVue
andSvelte
, you can still getHMR
on the client side.Making changes to server-side code requires completely re-executing the SSR entry point, which can be triggered through
HMR propagation
(this also applies toSSR
). But typically,HMR propagation
for server-side code will cause a full page reload, which is perfect for the client to re-send requests to the server and have the server perform re-execution.How to trigger a page reload in handleHotUpdate()?
The
handleHotUpdate()
API is designed to handle modules to be invalidated or handleHMR
propagation. However, there may be cases where changes are detected that require an immediate page reload.In
Vite
, you can useserver.ws.send({ type: 'full-reload' })
to trigger a full page reload, and to ensure modules are invalidated withoutHMR propagation
(which might incorrectly cause unnecessaryHMR
), you can useserver.moduleGraph.invalidateModule()
.jsfunction reloadPlugin() { return { name: 'reload', handleHotUpdate(ctx) { if (ctx.file.includes('/special/')) { // Trigger page reload ctx.server.ws.send({ type: 'full-reload' }); // Invalidate the modules ourselves const invalidatedModules = new Set(); for (const mod of ctx.modules) { ctx.server.moduleGraph.invalidateModule( mod, invalidatedModules, ctx.timestamp, true ); } // Don't return any modules so HMR doesn't happen, // and because we already invalidated above return []; } } }; }
Is there any specification for HMR APIs?
The only specification I know of is mentioned in this document, which has been archived.
Vite
initially implemented this specification but later deviated slightly, for example,import.meta.hot.decline()
was not implemented.If you're interested in implementing your own
HMR API
, you might need to choose between versions likeVite
orWebpack
. But essentially, the terminology for accepting and invalidating changes will remain the same.Are there other resources for learning about HMR?
Besides the Hot Module Replacement (HMR) documentation for Vite, Webpack, and Parcel, there aren't many resources that dive deep into how
HMR
actually works. However, here are a few resources I found helpful: