svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Svelte 5: better document breaking of universal reactivity with destructuring

Open denlukia opened this issue 1 year ago • 7 comments

Describe the bug

In a situation like this (see complete code in "Reproduction"):

<script>
	import { createPressWatcher } from './pressWatcher.svelte.js';

	const pressWatcher = createPressWatcher();
</script>

<button onmousedown={pressWatcher.onmousedown} onmouseup={pressWatcher.onmouseup}>
	Pressed: {pressWatcher.pressed}
</button>

it is tempting to refactor the code like this:

<script>
	import { createPressWatcher } from './pressWatcher.svelte.js';

	const { pressed, ...events } = createPressWatcher();
</script>

<button {...events}>
	Pressed: {pressed}
</button>

to reduce boilerplate.

But this breaks the app (events are working just fine, but pressed state is broken). As I understand it's because no dot-accessing is used and thus no getters are called, but this is not something a regular dev, who just copied reactivity pattern from docs and applied a common practice of destructuring, would immediately think about.

It is kinda of addressed in the docs with:

Note that we're using a get property in the returned object, so that counter.count always refers to the current value rather than the value at the time the createCounter function was called.

But the focus in this sentence seems to be on the fact of how we created not on the fact that we should consume this only with dot-access if we want to save reactivity. The user may start to look for a bug in "Universal reactivity module" but it's accessing code that should be changed.

Proposal Clearly state that for such universal reactivity case only dot-accessing works reactively

Reproduction

Working reactivity before destructuring

Broken reactivity after destructuring

Logs

No response

System Info

[email protected]

Severity

annoyance

denlukia avatar Mar 31 '24 06:03 denlukia

Destructuring can be made reactive via $derived, by the way:

const { pressed, ...events } = $derived(createPressWatcher());

REPL

brunnerh avatar Mar 31 '24 08:03 brunnerh

Although as a solution it feels a little cryptic I guess there could be use of documenting this too

denlukia avatar Mar 31 '24 10:03 denlukia

I mean if you think of it properly, destructuring properies is a form of reading a derived value. So it makes sense why it would need be to wrapped with $derived, no? I think we can improve documentation, so feel free to put up a RR for the Svelte 5 docs as to how you can make that better.

trueadm avatar Mar 31 '24 19:03 trueadm

How about:

Note that we're using a get property in the returned object and property accessors like counter.count in the template. This way we refer to the current value rather than the value at the time the createCounter function was called.

If you want to use destructuring instead of property accessors, wrap your call in $derived to save reactivity:

const { count, increment } = $derived(createCounter()); 

It would solve future reader's problem, but might also leave them with "Hm, but why it works this way?". I won't be able to answer that. Maybe some short explanation from the core team will be useful.

denlukia avatar Apr 01 '24 09:04 denlukia

I came across the same issue trying to debounce a value:

// debounce.svelte.ts
export const debounce = <T>(getValue: () => T, waitFor: number = 1000) => {
	let result = $state(getValue());
	let timeout: ReturnType<typeof setTimeout>;
	$effect(() => {
		const newValue = getValue();
		if (newValue === result) return;
		clearTimeout(timeout);
		timeout = setTimeout(() => {
			result = newValue;
		}, waitFor);
	});
	return {
		get value() {
			return result;
		}
	};
};

Where something like this worked:

<script lang="ts">
	let search = $state("");
	let debouncedSearch = debounce(() => search, 500);
</script>
<input type="text" bind:value={search}/>
<div>Debounced: {debouncedSearch.value}</div>

But a slightly more ergonomic attempt failed:

<script lang="ts">
	let search = $state("");
	let { value: debouncedSearch } = debounce(() => search, 500);
</script>
<input type="text" bind:value={search}/>
<div>Debounced: {debouncedSearch}</div>

Sadly in my case using $derived doesn't seem to work, because The Svelte $effect rune can only be used during component initialisation. At this point it's a minor issue for me (alternative suggestions are still welcome), but it did take me quite a while to figure it out.

nicksulkers avatar Apr 19 '24 06:04 nicksulkers

Oops, I didn't know that destructuring an object would lose reactivity. What's funny is that I use only stores with array $state, and in this case, destructuring doesn't cause reactivity loss.

Example: https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAA22QP2_DIBDFv8oJVYqtWkm7-p_UtUM7VJ3iDJTgFNUBdByVIsR3rzBJ7KET8H7v3XEX2Kgm6Vi9D0zzs2Q1e7GWVYwuNj3cr5xIsoo541EkpXUClaV-0ACj14KU0SCM1ySxKCEkHWCSlEXo4MERJ1k8lc3COOJC9oeEMkRJHvWtDMApm5fKKxNHbG5irNaJufW_mZn8k_L2yEnWUJTQ9eucMNqZSW4ncyo22bUpmzVPUz528LwSOeLWevddzHSxx3yJsxDzzKkBQUiZKlerrr-BCN2y2mbQ7e6-_EG3ts9xr6kO8xGTJclXFl4_3t-2jlDpkxovBUcs755Bt1-eyGgwWkxK_HQht409fOb-7S47elaxszmqUckjqwm9jIf4B5NWMiQ7AgAA

+1 to document that fact about destructuring and losing reactivity. It was a bit confusing for me.

mariansimecek avatar Apr 28 '24 14:04 mariansimecek

Oops, I didn't know that destructuring an object would lose reactivity. What's funny is that I use only stores with array $state, and in this case, destructuring doesn't cause reactivity loss.

Example: https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAA22QP2_DIBDFv8oJVYqtWkm7-p_UtUM7VJ3iDJTgFNUBdByVIsR3rzBJ7KET8H7v3XEX2Kgm6Vi9D0zzs2Q1e7GWVYwuNj3cr5xIsoo541EkpXUClaV-0ACj14KU0SCM1ySxKCEkHWCSlEXo4MERJ1k8lc3COOJC9oeEMkRJHvWtDMApm5fKKxNHbG5irNaJufW_mZn8k_L2yEnWUJTQ9eucMNqZSW4ncyo22bUpmzVPUz528LwSOeLWevddzHSxx3yJsxDzzKkBQUiZKlerrr-BCN2y2mbQ7e6-_EG3ts9xr6kO8xGTJclXFl4_3t-2jlDpkxovBUcs755Bt1-eyGgwWkxK_HQht409fOb-7S47elaxszmqUckjqwm9jIf4B5NWMiQ7AgAA

+1 to document that fact about destructuring and losing reactivity. It was a bit confusing for me.

That’s just how JS works. When you destructure you read the values at that point and assign them to variables. If the value is an object then you get a reference - which means you keep reactivity if that object is reactive.

trueadm avatar Apr 28 '24 15:04 trueadm