-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Description
Describe the bug
For runes to be a competent $store alternative/replacement, we need to be able to create $state inside a method, optionally react to it with any $effect/$derived and then return that proxified $state to a caller.
When creating a $state, then assigning it in a callback (either inside the callback of an $effect, an interval or other method) and returning it from the function, reactivity is lost, so any changes to that $state won't propagate out of scope. However, the $state is still reactive within the scope that it was defined in. REPL - minimal with $effect or REPL - with createInterval
// clock.svelte.js
export function createClock () {
let clock = $state('')
$effect(() => {
clock = new Date().toLocaleString()
})
// The state will be set locallly
$inspect(clock)
// but it won't be returned as such
return clock
}This effectively means that we cannot return the $state of a primitive, and the current behavior of $state locally and amongst functions or .svelte.js files seems inconsistent.
However, if we return the $state({}) of an object, we can see that, even though both assignments and mutations trigger reactivity within scope, only mutations get across the function scope. Working Object Mutation REPL
// mutation works
clock.timeString = new Date().toLocaleString()
// assignment only triggers reactivity in the function's scope, not on returned $state
clock = {
timeString: new Date().toLocaleString()
}
// mutation via Object.assign works again
Object.assign(clock, {
timeString: new Date().toLocaleString()
})What's most strange is that assignments to a $state do in fact propagate signals, but they can only be received in the scope the $state was defined in.
Can be worked around by wrapping the return value in an object and ensuring we only assign. Causes confusion when porting from Svelte 4 to 5, since we used to only assign and destructure to trigger reactivity obj = {...obj, value:1 }, and specifically that doesn't work.
Reproduction
Here's a Large REPL table with all 9 combinations:
- local
$state, function-returned$state, function-returned$statefrom a.svelte.jsfile - return
primitive, returnobjectand set inside fn, returnObjectand mutate inside fnObject.assign
And buttons to set the state externally, which behave exactly like the closures do.
What's less expected is that if we're assigning locally to that returned $state (columns 1, 2)
- if the function is inside the same file, assignments won't do anything (row 2)
- However, if the (exact same) function is imported from a
.svelte.jsfile, the assignment will overwrite the $state, making it diverge from what's inside the function scope. This probably warrants a second issue, once this is fixed.
Please note that wrapping the function return in a $state would be pointless, since in that case, the function's runes/listeners won't receive the event.
let functionState = $state(returnState());Logs
No response
System Info
svelte.dev playground, `svelte@5.1.0`Severity
blocking an upgrade