Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalid reactivity with $state declared in class field and new Imperative Component API #15683

Open
kposborne opened this issue Apr 4, 2025 · 0 comments

Comments

@kposborne
Copy link

kposborne commented Apr 4, 2025

Describe the bug

When using the imperative component API using a class in a svelte.js file, declaring the $state as a class field, then imperatively updating a prop, it did not react to changes properly. Notably, I found I could make it work if I added the $inspect rune to examine this prop

This was an annoyance when trying to migrate from svelte 4 to svelte 5 -> previously it could use $set() and that worked fine. The workaround was to fully migrate anything using the imperative API to runes mode, and initialize the $state in the .svelte file. The reason for using a class field is so that I can continue to use svelte components with ag grid -> for example here is their "ICellRenderer" interface, but here I provide a minimal example.

Reproduction

REPL Link

Code App.svelte
<script>
	import { onMount } from 'svelte'
	import { makeClass } from './utils.svelte.js'
	import Broken from './Broken.svelte'
	import FixedByInspect from './FixedByInspect.svelte'
	let divElement;
	let instances = []

	onMount(() => {
		for (const constructor of [Broken, FixedByInspect]) {
			const instance = new (makeClass(constructor))()
			instance.init()
			divElement.appendChild(instance.gui())
			instances.push(instance)
		}
	})
	
	function handleClick() {
		for (const instance of instances) {
			instance.update({ p1: true })
		}
	}
</script>

<div bind:this={divElement}></div>

<button on:click={handleClick}>Click Me</button>

Broken.svelte:

<script>
  let { p1 } = $props()
</script>

<div>
Broken Component Start
{#if p1 === undefined}
	<div>p1 is undefined, value={p1}</div>
{/if}
{#if p1 !== undefined}
  <div>p1 is not undefined, value={p1}</div>
{/if}
Broken Component End
</div>

utils.svelte.js


import { mount } from 'svelte'

export function makeClass(component) {
	return class {
		ps = $state({})
		div = undefined
		
		init() {
			this.div = document.createElement('div')
			mount(component, {
				target: this.div,
				props: this.ps
			})
		}
		
		gui() {
			return this.div
		}
		
		update(props) {
			Object.assign(this.ps, props)
		}
	}
}

FixedByInspect.svelte

<script>
  let { p1 } = $props()
  $inspect(p1)
</script>

Fixed Component Start
{#if p1 === undefined}
	<div>p1 is undefined, value={p1}</div>
{/if}
{#if p1 !== undefined}
  <div>p1 is not undefined, value={p1}</div>
{/if}
Fixed Component End

Initial State:

Image

After Clicking:
Image

Notice that the p1 === undefined branch contents is not removed as expected

System Info

I ran this in the svelte.dev playground - compiler version was 5.25.6

Severity

annoyance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant