Description
Describe the problem
It's a little irksome that this code, using the old Svelte 4 syntax, works as expected:
<script>
class FooBar {
constructor() {
this.foo = "abc";
this.bar = { stuff: [] };
}
}
let obj = new FooBar();
function addStuff(key) {
obj.bar.stuff = [...obj.bar.stuff, obj.bar.stuff.length]
}
</script>
<p>stuff</p>
<ul>
{#each obj.bar.stuff as stuff}
<li>{stuff}</li>
{/each}
</ul>
<button onclick={() => addStuff()}>add to bar</button>
but this, using Svelte 5's $state
, doesn't:
<script>
class FooBar {
constructor() {
this.foo = "abc";
this.bar = { stuff: [] };
}
}
let obj = $state(new FooBar());
function addStuff(key) {
obj.bar.stuff = [...obj.bar.stuff, obj.bar.stuff.length]
}
</script>
<p>stuff</p>
<ul>
{#each obj.bar.stuff as stuff}
<li>{stuff}</li>
{/each}
</ul>
<button onclick={() => addStuff()}>add to bar</button>
...when the only change between those two is the addition of a rune to explicitly declare the FooClass
instance as reactive (and therefore enabling runes mode). Essentially, reactivity no longer works on non-POJOs with Svelte 5's proxied reactivity, which has been a topic of discussion in the past. This is, as I understand, because using proxied reactivity on classes can introduce a lot of edge cases that could cause unexpected or wonky behavior, and I agree that it's a valid concern.
Now, the code from the second REPL above does still indeed work as expected in versions that predate the addition of proxied state to Svelte 5 (in particular, I've tried it on 5.0.0-next.15
), which makes the loss of intuitive nested reactivity on non-POJOs in local scopes seem like a regression - it used to work, and it still works if you use legacy syntax, but as of right now, after the introduction of proxied state, if you would like to adapt your code to the new way of doing things, you're forced to refactor entire class libraries to use $state
in their field declarations - thus tying them inextricably to Svelte and losing framework agnosticism - or write cumbersome boilerplate if you're using cross-framework code or dealing with external libraries you don't have full control over.
Describe the proposed solution
The proposal is to introduce a $state.direct
rune - or $state.noproxy
, or whichever name would be considered most suitable for this - the purpose of which would be to explicitly declare state to use whichever mechanism was in place to handle reactivity prior to the introduction of proxied state (and which, presumably, is still in place to handle reactivity in the case of Svelte 4 syntax - though do correct me if I'm wrong, as I'm not intricately familiar with the internal workings), and explicitly forgo said proxied state. This would massively simplify porting code that relies on reactive non-POJOs. The second REPL linked above would then look something like this:
<script>
class FooBar {
constructor() {
this.foo = "abc";
this.bar = { stuff: [] };
}
}
let obj = $state.direct(new FooBar());
function addStuff(key) {
obj.bar.stuff = [...obj.bar.stuff, obj.bar.stuff.length]
}
</script>
<p>stuff</p>
<ul>
{#each obj.bar.stuff as stuff}
<li>{stuff}</li>
{/each}
</ul>
<button onclick={() => addStuff()}>add to bar</button>
and provide the functionality one would expect when reading this code: that clicking the "add to bar" button would add a new entry to the listing of obj.bar
array items.
Importance
would make my life easier