Skip to content

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:

  1. import.meta.hot.accept(cb: Function) - Accept changes from itself
  2. 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:

ts
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;
  });
}
js
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:

js
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:

js
// 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:

ts
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:

js
// 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'));
        }
      }
    }
  };
}
js
// 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 that app.jsx is an accepted module. But before ending propagation, we need to determine if app.jsx can accept changes from stuff.js. This depends on how import.meta.hot.accept() is called.

    1. Scenario 1(a): If app.jsx is self-accepting, or it accepts changes from stuff.js, we can stop continuing up the propagation, because no other module depends on stuff.js module. Then the HMR client will notify app.jsx to execute HMR.
    2. Scenario 1(b): If app.jsx doesn't accept stuff.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 2: If updated main.js or other.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.

    1. Scenario 3(a): If app.jsx is self-accepting, we can stop here and let the HMR client notify this module to execute HMR.
    2. 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 4: If updated utils.js, propagation will again recursively find its parent dependency modules and all their ancestor dependency modules. First, we'll find app.jsx as an accepted module, and we'll stop propagation there (assuming it meets Scenario 1(a)). Then we'll also recursively find other.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. If at 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 that comp.jsx is an accepted module, and we'll handle it the same way as Scenario 1. To reiterate:

    1. Scenario 5(a): If comp.jsx is self-accepting, or comp.jsx accepts changes from stuff.js, then we can stop propagation. Then the HMR client will notify comp.jsx to execute HMR.
    2. 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 that app.jsx is an accepted module, and we'll handle it the same way as Scenario 5! Until we find a module that can accept stuff.js changes, if there's a branch that retrieves "root" index.html, then we need to perform a full page load.
  • Scenario 6: If updated bar.js, propagation will recursively find its importers and find that comp.jsx and alert.jsx are accepted modules. We'll also handle these two modules the same way as Scenario 5. Assuming the best case is that both comp.jsx and alert.jsx modules are Scenario 5(a), that is, both receive bar.js updates, then the HMR client will notify comp.jsx and alert.jsx two modules to execute HMR together.

  • Scenario 7: If updated utils.js, propagation will again recursively find its importers(importers) and find all direct importers comp.jsx, alert.jsx, and app.jsx, and all are accepted modules. We'll also handle these three modules(comp.jsx, alert.jsx, and app.jsx) the same way as Scenario 5. Assuming the best case is all accepted modules are Scenario 5(a), even if comp.jsx is also a HMR boundary part of app.jsx, the HMR client will notify them three to execute HMR. (Future, Vite may detect this and only notify app.jsx and alert.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 if comp.jsx is self-accepting.

    1. Scenario 8(a): If comp.jsx is self-accepting, then we stop propagation and let the HMR client notify comp.jsx to execute HMR.
    2. Scenario 8(b): If comp.jsx is not self-accepting, then we can handle it the same way as Scenario 5(b).

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:

  1. Establish a WebSocket connection with the Vite dev server.
  2. Listen for HMR payloads from the Vite server.
  3. Provide and trigger HMR APIs in the runtime phase.
  4. 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:

js
// /@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:

tsx
// 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.

js
// /@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:

  1. connected: Sent when a WebSocket connection is established.
  2. error: Sent when an error occurs on the server side, Vite can display the specific error content in the browser console.
  3. 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:

ts
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:

ts
// /@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:

js
// 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:

ts
// 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().

ts
// 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.

ts
// /@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:

js
// /@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.

js
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:

ts
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:

ts
// 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:

  1. How to use HMR APIs to handle file changes.
  2. How file editing causes the Vite dev server to send HMR updates to the HMR client.
  3. How the HMR client processes HMR payloads and triggers the correct HMR APIs.

Before we end, please check out the FAQ below if you still have questions about how certain things work.

FAQ

  1. Where can I find the source code for Vite's HMR implementation?

  2. Are there any HMR examples to learn from?

    HMR is typically implemented by JS frameworks that introduce the concept of "components", where each component can isolate its state and reinitialize itself. Therefore, you can look at how frameworks like React, Vue, and Svelte implement them:

  3. How does Vite's implementation differ from Webpack and other tools?

    I haven't delved deep into Webpack's implementation, only understanding Webpack's HMR principles from the Webpack documentation and this NativeScript article. As far as I know, a common difference is that Webpack handles HMR 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 the HMR APIs used in modules on the server side to determine if a module has called import.meta.hot.accept(). However, handling HMR 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.

  4. How does HMR work in server-side rendering?

    At the time of writing this article (Vite 5.0), HMR in SSR is not yet supported, but will be released as an experimental feature in Vite 5.1. Even without HMR in SSR, for JS frameworks like Vue and Svelte, you can still get HMR 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 to SSR). 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.

  5. How to trigger a page reload in handleHotUpdate()?

    The handleHotUpdate() API is designed to handle modules to be invalidated or handle HMR propagation. However, there may be cases where changes are detected that require an immediate page reload.

    In Vite, you can use server.ws.send({ type: 'full-reload' }) to trigger a full page reload, and to ensure modules are invalidated without HMR propagation (which might incorrectly cause unnecessary HMR), you can use server.moduleGraph.invalidateModule().

    js
    function 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 [];
          }
        }
      };
    }
  6. 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 like Vite or Webpack. But essentially, the terminology for accepting and invalidating changes will remain the same.

  7. 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:

    What the heck is HMR anyway? - Pedro Cattori (YouTube)

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (dbcbf17)