Vite Hot Module Replacement - A Complete Example
Tags: javascript, vite, webdev, productivity
Published: 2022-04-17 15:45:21

This article explains how to set up hot module replacement (HMR) with Vite for a vanilla JS project. The goal is to be able to see changes in the code without refreshing or restarting the application.

For me this is particularly useful in creative coding when I'm iteratively building a simulation and don't want to lose state as I make live changes.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a08hchvm37reavpfydxn.gif)
 
Complete source code for this article here: https://github.com/OmarShehata/vite-hot-reload-example. 

## Overview of my implementation

I'm going to show you how I've set up my HMR to make it as simple as possible to enable for new modules. In subsequent sections I'll break down how it works so you are aware of all the caveats and can use it in your project as needed.

To enable HMR on any module, I add the event handler at the top of the file:

```javascript
import { HMREventHandler } from './HotModuleReloadSetup.js';

if (import.meta.hot) {
  import.meta.hot.accept(HMREventHandler)
}
```

This tells Vite to avoid refreshing the page when this module is updated, and instead fire an HMR event.

Next, I have a singleton instance of `HotModuleReloadSetup` that takes dynamically imported modules, and automatically swaps them out:

```javascript
import HotModuleReloadSetup from './HotModuleReloadSetup.js';
// Setup HMR
const hmr = new HotModuleReloadSetup();
// Load a module that will be updated dynamically
hmr.import(await import('./Draw.js'));
```

I now have access to an instance of the Draw class through `hmr.instances['Draw']`.

Finally, the draw class defines a `hotReload` handler. This passes a reference to the old instance so you can copy over any state variables to the new instance.

So when I call `hmr.instances['Draw'].draw()` in the render loop, it will always use the latest code.

This pattern is modelled after how [PlayCanvas does their hot reloading](https://developer.playcanvas.com/en/user-manual/scripting/hot-reloading/).

## How it works

Here is what happens when you make a change in an HMR module (`Draw.js` in my case) & save the file:

1 - Vite triggers an HMR event (which we've added a listener to via `import.meta.hot.accept`)
2 - I then dispatch a custom event on the DOM with that new module (this is in [HotModuleReloadSetup.js](https://github.com/OmarShehata/vite-hot-reload-example/blob/f831efd75129de25392e023d12fc2a171545c312/src/HotModuleReloadSetup.js#L36)):

```javascript
export function HMREventHandler(newModule) {
	const event = new CustomEvent('hot-module-reload', { detail: { newModule } });
	document.body.dispatchEvent(event);
}
```

This allows `HotModuleReloadSetup` to listen for these events globally.

3 - We search our map of `modules` to see if the new module we received exists
4 - If so, we create a new instance from the new module, call `newInstance.hotReload(oldInstance)`, and discard the old module & old instance

```javascript
swapModule(newModule) {
  const name = newModule.default.name;
  const oldModule = this.modules[name];
  const oldInstance = this.instances[name]
  if (!oldModule) return;

  const newInstance = new newModule.default();
  newInstance.hotReload(oldInstance)

  this.modules[name] = newModule
  this.instances[name] = newInstance
}
```

This way you can access either one instance of a class that is always updated through the `instances` map, or you can access the latest module through the `modules` map. The latter can be used if your code is spawning many instances of a class (like enemies or bullets), and you want your live code changes to affect newly spawned instances.

## Caveats 

* If you need to have multiple instances of a class that are all updated when the code changes, you'll need to keep an array of these instances and swap them all out in the same way `HotModuleReloadSetup.swapModule` does
* If your HMR class needs constructor arguments, you'll need to store those in the global manager that creates the new instances so they can be passed to them as well.
* I turn off Rollup minification because it rewrites function names, but cannot update all uses of the function when using dynamic imports like this. That causes `hmr.instances['Draw'].draw()` to break because the function name is no longer `draw` in a production build.
* I import the module by name and pass it to `hmr.import()` due to the limitations on dynamic imports in Vite, see: https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations

----

Thanks for reading! I hope you found this useful. If you find a bug or a better way to streamline HMR, especially for creative coding, I'd love to hear. You can find me on Twitter: https://twitter.com/omar4ur.