svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Svelte 5: Bring back $state.link

Open HighFunctioningSociopathSH opened this issue 1 year ago • 8 comments

Describe the problem

Knowing that $state.link existed once, I can't stop thinking how many times I could use it in my project. It's really useful and removes the need of having to write an extra $effect that does nothing other than assigning a value to a variable. Also the work around that uses an $effect isn't really good either because the assignment inside $effect is not fine-grained.

Describe the proposed solution

I think it would be nice to have it back and makes migrating from svelte 4 easier since in svelte 4 you could assign to the reactive variable and it would update again if its dependency changed. For example, you could do the following in svelte 4 when you didn't want to change the original data passed by the user and still update your finalData if it got updated

export let data;
$: finalData = data;
function handleOnClick() {
  finalData = "something";
}

So here finalData was updated internally but it would have been updated again if the user decided to update the data prop. This could translate to the following in Svelte 5 which is neat:

let { data } = $props();
let finalData = $state.link(data);
function handleOnClick() {
  finalData = "something"
}

Importance

nice to have

For reference: https://github.com/sveltejs/svelte/pull/12938#issuecomment-2299767265 here is the decision to remove $state.link.

The problem was that, after correcting the behaviour, it used the $effect internally anyway. Also, there were open questions about behaviour in SSR and the inability to apply $state.link to an object's prop. A workaround can have whatever behaviour you need.

Examples: sync $state.link and $state.link with async merging. If you want, you can wrap the logic into the Box pattern and have something like

let box = link(() => parentValue, onMerge);
console.log(box.value);

7nik avatar Sep 30 '24 16:09 7nik

Couldn't you just use a $derived, like this?

let a = $state(0);
let b = $derived(a);

Ocean-OS avatar Oct 02 '24 15:10 Ocean-OS

Couldn't you just use a $derived, like this?

let a = $state(0);
let b = $derived(a);

No, derives are read-only (though mutable). So you cannot do b++ or b = 42.

7nik avatar Oct 02 '24 16:10 7nik

No, derives are read-only (though mutable). So you cannot do b++ or b = 42.

Ah, I see. Then wouldn't something like this work?

let a = $state(0);
let b = $state($state.snapshot(a));
$effect(()=>{
b = $state.snapshot(a);
});

It shouldn't be too much of a problem that $effect is being used here since that was what $state.link used internally.

Ocean-OS avatar Oct 02 '24 19:10 Ocean-OS

The problem was that, after correcting the behaviour, it used the $effect internally anyway.

It didn't. It used a derived. We wanted to not ship something that we regretted and given how close we are to 5.0 release, we felt like doing this work after release was a better use of time.

trueadm avatar Oct 07 '24 13:10 trueadm

The problem was that, after correcting the behaviour, it used the $effect internally anyway.

It didn't. It used a derived.

Initial implementation — yes, but it was buggy, and the PR I've linked was fixing it by using effect instead of derived.

7nik avatar Oct 07 '24 14:10 7nik

@7nik The effect wasn't the fix, it was just a bug in the initial implementation.

trueadm avatar Oct 07 '24 14:10 trueadm

Here's a workaround that avoids effects but has manual resetting, the verboseness has some clarity:


value; // maybe from a prop, `type T`

let local_value: T | undefined = $state(value); // or initialize to `undefined` or something else

const final_value = $derived(local_value === undefined ? value : local_value); // `type T`

value = something; // update the source of truth
local_value = something; // update just the final linked binding
local_value = undefined; // revert the final linked binding to the source of truth

This means your handlers may sometime need a 2-liner to both revert the linked binding and update the source of truth, a pattern that's been talked about in these discussions - you can have these updates in a single-code-path helper to mitigate the error-proneness.

This could be wrapped in a helper API with explicit methods to improve usage.

ryanatkn avatar Oct 07 '24 18:10 ryanatkn

Regardless of what implementation is used or what issues have to be addressed, people will need this mechanism. There are too many cases where a single source of truth (for example the server) provides data which has to be mutable for editing. In one of my projects I implemented reordering elements of a list which was saved in a database using exactly this kind of functionality (a seperate variable assigned by an effect). Otherwise, I wouldn't have been able to animate the drag and drop.

Since people need this feature and they shouldn't stumble into the problems of SSR and nested assignments, a seperate rune with thorough documentation would be really helpful. But maybe naming it something else should be considered, for the state is not really linked, but it rather expresses a possible future state of the data. Some funny ideas I had were $potential() or $imagine(), but more technical terms like $grounded() or $dependent() would do the trick too.

I was thinking that it would also be useful to provide a mechanism for resetting the "dependent" state to the source of truth without repeating oneself. Maybe a $ground() or a $resync() rune would be possible.

Siuhnexus avatar Nov 19 '24 10:11 Siuhnexus

I've come back to say:

I find myself often using something like $derived to get shorter variable identifiers in my code and templates

let arr = $derived(this.deeply.nested.array_thing) // Not reactive for setting

arr = array.filter(...) // wouldn't work

So yes, having something like:

let arr = $state.link(this.deeply.nested.array_thing)

Would be really nice

AlbertMarashi avatar Jan 17 '25 10:01 AlbertMarashi

Theoretically, if #12956 were to be made, wouldn't that provide an easy way to recreate $state.link?

let arr = $state.from({
get: ()=>this.deeply.nested.array_thing,
set: v=>this.deeply.nested.array_thing=v
});

Ocean-OS avatar Jan 17 '25 16:01 Ocean-OS

Addressed in https://github.com/sveltejs/svelte/pull/15570

benmccann avatar Mar 21 '25 19:03 benmccann