Skip to content

Instantly share code, notes, and snippets.

@lacikawiz
Last active March 20, 2023 20:14
Show Gist options
  • Select an option

  • Save lacikawiz/92136112b9ae2deb47492b7734e41403 to your computer and use it in GitHub Desktop.

Select an option

Save lacikawiz/92136112b9ae2deb47492b7734e41403 to your computer and use it in GitHub Desktop.
SolidJS with Vite: preventing multiple instances of code running and HTML generated when modules are hot reloaded

I've started using SolidJS maybe 2 months ago, and I like it very much. It has solutions to problems I encountered with Svelte while building a large Apps. But there's one annoyance that comes up during the development mode which bugged me until I found a solution.

If you have been working with SolidJS using the developer mode (npm run dev) then you probably noticed the annoyance which happens when you modify a file, Vite reloads it but parts of the old code is still active (namely the reactive effects), so the HTML can have duplicates, old errors that you fixed cropping up, etc. Beyond annoyance it can waste a lot of developer time to try to figure why something is still malfunctioning after fixing it.

I've run into this myself quite a bit so I decided to dig deeper. After extensive Google-ing I couldn't find the mention of this issue or any solution. It's sure to exist because it happens even when I use SolidJS' standard starter template with a very simple application.

Anyways, problem lies in the fact that during hot module reloading things are not getting disposed. Namely the "roots". I had to dig deeper for this information and do a bunch of trial and error experiments in order to figure out what are these and how they work. The documentation is pretty scarce on it and the other articles mentioning it are giving too complex examples to make it easy to understand it.

A "root" is a context that contains the reactive computations that are created within it using createEffect or other means. When a root's lifecycle ends these reactive computations are destroyed, freeing memory and resources. They are created using the createRoot function in the solid-js library. Ref: https://www.solidjs.com/docs/latest/api#createroot

Unfortunately, there's no example for this and the other places also don't mention what the dispose supposed to and even it's omited half the time in the examples. So here's an example that actually uses it:

const [foo,setFoo] = createSignal("")
const [bar,setBar] = createSignal("")

//...

let disposer

createRoot((disposeFn)=>{
  disposer=disposeFn
  createEffect(()=>{
    setBar("foo:"+foo())
  })
})

function stopIt(){
  if(disposer) disposer()
}

Ok, it's a bit of a contrived example but I wanted to keep it simple and not introduce a complex setup. Let's see how it works:

  • We create 2 signals: foo and bar
  • We set up link between these so that when foo changes then bar is updated with the content of foo prefixed with "foo:"
  • This reactive pattern is encapsulated in a root and during the creation of that root we save the dispose function that SolidJS provided for it
  • We define a stop function that would stop linking the two signals. This consist of simply calling the previously saved dispose function

The next step in understanding is that the render function that kickstarts your app, is also using the createRoot call under the hood and if you look at it well, you notice that it returns a function. This function is the dispose function for the whole app created by the render.

Now, that you understand what's happening under the hood, let's get back to the solving the problem of multiple instances.

First of all, we need to save the dispose function returned by render:

const appDisposer = render(<MyApp/>, document.querySelector(".app-container"))

Next problem(s):

  • How do we know when the module is reloaded and this is not the first instance?
  • How do we save dispose function so that it survives the reload?

I digged a little bit and found some Vite specific data here: https://vitejs.dev/guide/api-hmr.html (HRM = Hot Module Reloading)

Basically when working with Vite, then we will have an object available at import.meta.hot which contains some useful things for working with HMR. The solution I opted for is using the data field (import.meta.hot.data) which survives the reloading.

So let's modify the above code to save the dispose function to this:

let HMRdata={}
if(import.meta.hot) HMRdata=import.meta.hot.data

HMRdata['appDisposer'] = render(<MyApp/>, document.querySelector(".app-container"))

Now, we have the dispose function saved that survives the reloading. All we need to add is to check if this value exists (was saved from a previous run) and if it does then calls the dispose function.

let HMRdata={}
if(import.meta.hot) HMRdata=import.meta.hot.data
if(HMRdata['appDisposer']) HMRdata['appDisposer']()

HMRdata['appDisposer'] = render(<MyApp/>, document.querySelector(".app-container"))

That's it! After this there will be no double, triple, etc copies of HTML elements when you edit a file.

I hope that some kind of a similar handling will be added in SolidJS code, to automatically take care of the render calls which usually create mutliple copies of the HTML being displayed.

Based on the above I created two functions to simplify using this process and allow multiple dispose functions in one module to be handled. The file below contains these, which you can add to a commonly imported .tsx file.

Note #1: The functions need the import.meta.hot passed in to them because import.meta is a special object that is unique to the module running the code, so if it was called from imported module then its value would correspond to the the imported module, and not the module calling the function).

Note #2: The disposeAll should be called early in a module to do the cleanup before the new ones are added.

// The property name used inside the `import.meta.hot.data` object to store the
const disposers = 'appDisposers'
// note: I couldn't figure out how to do this in one step
const IMH = import.meta.hot
type tIMH = typeof IMH
export function addDisposer(importMetaHot:tIMH,disposer:CallableFunction){
if(!importMetaHot || !importMetaHot.data) return
const D = importMetaHot.data
if (!D[disposers]) D[disposers]=[]
D[disposers].push(disposer)
}
export function disposeAll(importMetaHot: tIMH):void{
if (!importMetaHot || !importMetaHot.data) return
const Ds = importMetaHot.data[disposers] as CallableFunction[]
if(Ds) Ds.forEach(d=>d())
}
/* Using it with `render` */
import {disposeAll,addDisposer} from "./disposeHandlers"
disposeAll(import.meta.hot)
// ...
addDisposer(import.meta.hot,render(() => <Layout/>, document.querySelector(".app-container")))
/* Using it with `createRoot` */
import {disposeAll,addDisposer} from "./disposeHandlers"
disposeAll(import.meta.hot)
// ...
createRoot((disposeFn)=>{
addDisposer(import.meta.hot,disposeFn)
createEffect(()=>{
setBar("foo:"+foo())
})
})
@lacikawiz
Copy link
Author

๐Ÿ˜ƒ Thank you! I'm very glad you found this and it helped you!

@madil4
Copy link

madil4 commented Mar 20, 2023

Saved me from a lot of hustle, thanks ๐Ÿ™‚

@lacikawiz
Copy link
Author

You are welcome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment