svelte
svelte copied to clipboard
Svelte 5: Bring back $state.link
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);
Couldn't you just use a $derived, like this?
let a = $state(0);
let b = $derived(a);
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.
No, derives are read-only (though mutable). So you cannot do
b++orb = 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.
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.
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 The effect wasn't the fix, it was just a bug in the initial implementation.
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.
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.
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
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
});
Addressed in https://github.com/sveltejs/svelte/pull/15570