Skip to content

Proposal: a way to make non-POJOs reactive #10560

Open
@ThePaSch

Description

@ThePaSch

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions