Svelte 5 $state rune Class vs Object reactivity
Svelte 5 was finally released after a long development cycle, and it brings the concept of runes, which changes the state paradigm quite a bit compared to Svelte 3/4. I stayed away from Svelte during this period, because I wanted to work with the finished product.
Playing around with the $state
rune, I found that I generally like it. It was a lot harder to use it in a global, sharable store (because the official documentation is lacking in this respect), but a friendly Svelte contributor on bsky helped me with a working example.
I'm building a tiny app that uses a shared store to hold arrays of objects. This is both to test Svelte 5's new features, and to scratch an idea I had.
Now, the objects in my arrays are actual JavaScript class instances. I thought it makes sense to use classes instead of simple objects, because there are some OOP features here that can come in handy (in particular having logic in the constructor).
I got stuck on an issue though. When I updated a property of an object (class instance) in the array (incrementing a counter), it wasn't responsive. It took a while to figure out (my JS isn't my strong suit).
Long story short, it turns out that if you store a class instance using $state
, its properties are not reactive. While you can modify the internal state of the object, it won't re-render in the browser.
Note You can, in fact, make class properties reactive. Read all the way down for the best solution.
So what can you do? I found 2 solutions (you might know more):
- store a simple object instead of a class -
{}
instead ofnew Widget()
- or add a
toObject
method to the class which returns the properties you need, as a simple object (the implementation here might be naive, but it does what I need so I don't care)
Since I was adamant to use classes, I decided on the 2nd solution.
Here's the code, to illustrate what's happening. You can also find it in the REPL.
<script>
let collection = $state([])
class Ob {
id = null
name = ''
count = 1
constructor(id, name) {
this.id = id
this.name = name
}
toObj() {
return {
id: this.id,
name: this.name,
count: this.count,
}
}
}
function incr(i) {
collection[i].count++
}
collection.push(new Ob(1, '❌ Class'))
collection.push({ id: 2, name: "✅ Object", count: 1 })
collection.push(new Ob(3, '✅ Class.toObj()').toObj())
</script>
{#each collection as col, i}
<button type="button" onclick={() => incr(i)}>+</button>
{JSON.stringify(col)}
<br>
{/each}
And here's a GIF of the 3 scenarios.
I hope this is helpful in case you run into the same problem. It sure enough got me unstuck and helped me understand Svelte 5's reactivity just a tiny bit more.
But wait, there's more. It turns out you can actually make the 1st scenario (Class properties) reactive by wrapping the class properties in $state
. Who woulda thunk it? Here's an updated REPL. There's no need for the toJSON
function with this approach.
<script>
let collection = $state([])
class Ob {
id = $state(null)
name = $state('')
count = $state(1)
collection.push(new Ob(1, '✅ Class'))
Thanks to Pablopang.svelte over on Bluesky for helping with this!
Mea Culpa I must be a dumbass. After all this, I discovered that Class reactivity is clearly laid out in the docs. I could swear it wasn't there the first 5 times I read that entire page. But it was - my brain probably skipped it for some reason. So all I did here was to find a couple of non-optimal solutions and waste someone's time on social media, when I could have just RTFM more carefully 🤦♂️