How Vite’s HMR API Works for a Component Workbench

These are just brief notes and don’t do justice to the topic but here’s a start.

Base Understanding

You must first understand Hot Module Reloading. It’s pretty mystical at first but it’s really not that hard to understand. Start by reading:

Key Takeaways

Svelte but not TS/JS files self accept hot updates

You need to understand that each file can either self-accept or not. If a file can’t accept its hot update, the update bubbles up to whatever modules imported it and so on up the chain. If one of its import branches is never “caught” by being accepted and the update bubbles all the way up to the root module then a full page reload will be triggered (svelte-hmr). At this point then, HMR has given us no benefit from a server without such feature.

Thanks to vite-plugin-svelte and svelte-hmr, Svelte files accept their own hot updates and apply appropriately (in your developer console’s “Sources” tab you can inspect your compiled Svelte files and see the HMR additions). Children components have to be remounted however as it’s too difficult to programmatically know how to hot swap them.

Typescript files do not self accept of their own accord however and will trigger a full reload if no one catches them. If they are imported by a Svelte file, then that Svelte file will catch their update saving you from a full reload. If they are imported by a layout.ts or page.ts file for example, then any changes will trigger a full reload. Since Kitbook utilizes Vite’s glob imports feature, a full reload is triggered every time a variant is update, leading to a horrible experience. Other workbench tools don’t struggle with this issue because they go through the work of analyzing the file tree themselves. But that takes a lot of code and Vite has already done the work for us so why not take advantage of it?

Before going further, add a simple plugin to log hot update and see in action what you’ve been reading about:

vite.config.js
ts
import type { Plugin } from 'vite';
function logHotUpdate(): Plugin {
return {
name: 'log-hot-update',
handleHotUpdate({ modules }) {
console.log({ modules });
}
}
}

If you edit a typescript file you notice isSelfAccepting: false but if you edit a svelte file you’ll it is true.

You can manually accept hot updates

A major feature of our Kitbook is that we can quickly create component variants simply by editing an array of props objects in a Typescript file. On initial page load these are being imported in server side load functions to be able to do server side rendering of components. These are provided by a glob import:

moduleGlobImport.ts
ts
export const modules = import.meta.glob(['/src/**/*.{md,svx,svelte,variants.ts}', '/README.md']);

Then we catch hot updates to these files and add them to a store which will replace the modules used on first load and smoothly avoid full page reloads.

moduleGlobImport.ts
ts
import { modulesForKitbookStore } from "./hmrUpdatedModules";
export const modulesForKitbook = import.meta.glob(['/src/**/*.{md,svx,svelte,variants.ts}', '/README.md']);
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
modulesForKitbookStore.set(newModule.modulesForKitbook);
})
}

The Final Piece: Extracting and Hot-Reloading Each Story from a stories file

So we now have hot reloading for Svelte files (like normal), for variants arrays provided by Typescript files (and incidentally raw strings of modules imported using as: raw also do not self-accept), but we haven’t yet solved the problem of when a stories file is saved it has to reload all of it’s children stories since they are iframes placed in children components. We add a function that takes the code of a stories file and saves an individual story file to src/kitbook/stories. On a stories file’s first load this function is called by Vite’s transform hook as the story is loaded, the value of that file without the stories is then saved to a map inside the plugin. The function is also called inside handleHotUpdate for that stories file where it will compare to check if something outside of a story has changed. If nothing the hook will return an empty array to negate hot reloading the stories file. It will have taken the internal changes to a particular story and update the corresponding file which will kick in HMR just for that story.

A Gotcha

You can’t add undefined to a file in a library that will be run from inside of node_modules. Vite caches those files and so hot updates will be useless.

Quiz question

In an ordinary SvelteKit app would updating a Typescript file only imported in a Svelte file bubble up to a full page reload? No, but it does cause that specific Svelte file to hot update itself which will cause children components to remount.

Edit page on GitHub