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.
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:
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:
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.
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):
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
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 longerdraw
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.
Top comments (2)
So if I understand correctly I need to enable HMR for every module manually? Seems like a lot of work.
Maybe it would save time if you're focussing on a single module that has a lot of internal state. Otherwise I would just setup Storybook and create stories for every individual state of the component.
I agree that Storybook sounds great for UI components. I think this model is less work for interactive/3D applications where parts aren't easily extracted out, and helpful if you need to see it in full context.