Description
Describe the problem
Deeply reactive state is great, but the simplicity of Svelte 4 is something that I still can't find ways to bring back. Consider this:
const [debounced, instant]= debounced({ prop: 123 })
// this triggers the debounced store to change after a delay
$instant.prop = 456
You don't really need to know the implementation of debounce()
to understand how it might work, subscribing to the first store and setting the second's value after a timeout. For complicity, here's the implementation:
export function debounced<T>(initial: T, delay: number = 1000): [Writable<T>, Writable<T>] {
let timeout: ReturnType<typeof setTimeout>
const debounced = writable(initial)
const instant = writable(initial)
// update() implementations omitted
return [
{
subscribe: debounced.subscribe,
set(value) {
clearTimeout(timeout)
debounced.set(value)
instant.set(value)
},
},
{
subscribe: instant.subscribe,
set(value) {
clearTimeout(timeout)
instant.set(value)
timeout = setTimeout(() => debounced.set(value), delay)
},
},
]
}
Now, I've been trying to convert this to Svelte 5, without too much success. My requirements are:
- clean implementation (no effects),
- must work with nested properties.
After all, this is why I loved Svelte so much over React - no need to worry about state or creating new objects/arrays everytime you assign. And yet, ever since I started working with Svelte 5, this is all I am allowed to think about. I remember it took me around 15 minutes to come up with the above implementation and I thought that stores are nice. How about state, tho? I managed to quickly make a function to respect my first condition:
export function debounced<T>(initial: T, delay: number = 1000): { debounced: T; instant: T } {
let debounced = $state.raw(initial)
let instant = $state.raw(initial)
let timeout: ReturnType<typeof setTimeout>
return {
get debounced() {
return debounced
},
set debounced(newValue) {
clearTimeout(timeout)
debounced = newValue
instant = newValue
},
get instant() {
return instant
},
set instant(newValue) {
instant = newValue
clearTimeout(timeout)
timeout = setTimeout(() => (debounced = newValue), delay)
},
}
}
This looks great, but what about the second condition, that this should work with nested properties like it used to? I could make it work with some effects like:
$effect.root(() => {
$effect(() => {
// listen to changes anywhere in the state
void $state.snapshot(instant)
// propagate the change
setTimeout(() => (debounced = newValue), delay)
})
})
This might work, but I have so many questions:
- Does this cause a memory leak, or can the effect root be GC'd?
- If the answer to the above question is true, how can this be achieved instead?
- If state is supposed to simplify stores, why did I have to learn 3 complex and weird runes and their quirks just to achieve 10% of what stores can without any knowledge whatsoever?
- Why?
Maybe this is just a skill issue for me, but this is like the 3rd time I run into such a roadblock while trying to migrate my code. Most of the type I'm just trying to decide between an object with a current
property, a function or God knows what when I could just use a store. I understand stores aren't deprecated per-se, but with $app/stores
being deprecated in Kit it's tough to say what the intended direction is.
Describe the proposed solution
Thr dollar sign abstractions in Svelte 4 were the closest thing we ever got to world peach, and with every rune we stray further from God. I whole-heartedly understand why runes, but it is frustrating to 10x complicate my code when it's supposed to be the opposite, They're amazing on paper and in demos, not so much in practice.
What do I want? Not sure, but some better documentation on how to use them, when to use current
versus functions, a bunch of examples for more complex cases. Maybe I just wanted to vent a little.
Importance
nice to have