-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Skill Issue] State and runes are too difficult #14978
Comments
$watch
rune
$watch
rune
You can't dismiss runes because of one "good" example of a bad consequence. Sure, I see your point: With "$", Svelte would call The good news is that you may stay as you are and continue using stores, as they don't have a foreseeable deprecation date. Moreover, your use case can even solidify the need to continue keeping stores around. I believe that runes do simplify many more scenarios than it complicates, with the added benefits of performance gains and the ability to refactor code. Regardless of what the core team thinks or may have thought, this is a good example/reminder that signals may not be the one solution for everything. |
Make use of export class Debounced<T> {
#current = $state() as $state.Snapshot<T>;
constructor(get: () => T, delayMs: number) {
this.#current = $state.snapshot(get());
$effect(() => {
const value = $state.snapshot(get());
const timeout = setTimeout(() => {
this.#current = value;
}, delayMs);
return () => clearTimeout(timeout);
});
}
get current() {
return this.#current;
}
} <script lang="ts">
import { Debounced } from '$lib/utils/state/debounced.svelte';
let value = $state({ email: '', age: 18 });
const debounced = new Debounced(() => value, 300);
</script>
<div class="flex flex-col gap-2 mx-auto">
<input class="border" type="text" bind:value={value.email} />
<input class="border" type="number" bind:value={value.age} />
<pre>{JSON.stringify(debounced.current, null, 2)}</pre>
</div> |
Or alternatively: export class Debounced<T> {
value = $state() as T;
#current = $state() as $state.Snapshot<T>;
constructor(initial: T, delayMs: number) {
this.value = initial;
this.#current = $state.snapshot(initial);
$effect(() => {
// create a snapshot to make the debounced state deeply reactive
const value = $state.snapshot(this.value);
const timeout = setTimeout(() => {
this.#current = value;
}, delayMs);
return () => clearTimeout(timeout);
});
}
get current() {
return this.#current;
}
} <script lang="ts">
import { Debounced } from '$lib/utils/state/debounced.svelte';
const debounced = new Debounced(
{
email: '',
age: 18
},
300
);
</script>
<div class="flex flex-col gap-2 mx-auto">
<input class="border" type="text" bind:value={debounced.value.email} />
<input class="border" type="number" bind:value={debounced.value.age} />
<pre>{JSON.stringify(debounced.current, null, 2)}</pre>
</div> |
Or use the runed package for various utilities |
Hi @david-plugge, thanks for the solution! Unfortunately, I cannot directly use |
In that case you may use import { createSubscriber } from 'svelte/reactivity';
export class Debounced<T> {
#current: $state.Snapshot<T>;
#subscribe: () => void;
constructor(get: () => T, delayMs: number) {
this.#current = $state.snapshot(get());
this.#subscribe = createSubscriber((update) => {
return $effect.root(() => {
$effect(() => {
const value = $state.snapshot(get());
const timeout = setTimeout(() => {
this.#current = value;
update();
}, delayMs);
return () => clearTimeout(timeout);
});
});
});
}
get current() {
this.#subscribe();
return this.#current;
}
} |
A complete example could look like this: import { createSubscriber } from 'svelte/reactivity';
export class Debounced<T> {
#get: () => T;
#current: $state.Snapshot<T>;
#subscribe: () => void;
#updateFn: (() => void) | null = null;
constructor(get: () => T, delayMs: number) {
this.#get = get;
this.#current = $state.snapshot(get());
this.#subscribe = createSubscriber((update) => {
return $effect.root(() => {
$effect(() => {
const value = $state.snapshot(get());
this.#updateFn = () => {
this.#current = value;
update();
cleanupFn();
};
const cleanupFn = () => {
this.#updateFn = null;
clearTimeout(timeout);
};
const timeout = setTimeout(this.#updateFn, delayMs);
return cleanupFn;
});
});
});
}
get current() {
if ($effect.tracking()) {
this.#subscribe();
return this.#current;
}
return this.#get();
}
updateImmediately(): Promise<void> {
return new Promise((resolve) =>
setTimeout(() => {
this.#updateFn?.();
resolve();
}, 0)
);
}
} |
I suppose that @david-plugge 's solution is proving the original point: Signals and fine-grained reactivity work against the desired coarse reactivity. If I were @rChaoz, I would continue using the store implementation. |
Why create an effect root and an effect inside subscriber? The whole point of create subscriber is to initialize listeners and clear them when they are not needed anymore. Also in general the coarse grained reactivity you want can be achieved with a custom recursive proxy |
I don't want course reactivity. But @david-plugge's great solution does a few important points:
All in all, it feels like something is missing or wrong, but I can't say exactly what it is. |
I don't know if you're missing the point: Svelte v5 uses fine-grained reactivity, which simplifies well over 90% of all code bases; your case is particular, special: It benefits from the old coarse-grained reactivity which happens to be the strong point in Svelte v4. Just stick to stores. They aren't deprecated. |
In the end, this is the solution I used (repl): type DebouncedState<T> = {
/**
* The debounced value, available after a delay.
* Setting this property is also reflected immediately in `instant`.
*/
debounced: T
/**
* The instant value, which propagates to `debounced` after a delay.
*/
instant: T
}
/**
* Returns an object with two **raw** reactive properties - **`debounced`** and **`instant`**, both starting with the given initial value.
* Changes performed to `instant` will be forwarded to `debounced` after the specified delay.
* If multiple changes happen in quick succession (less time between them than the specified delay), only the last change is forwarded.
* Changes performed to `debounced` are instantly reflected in properties.
*
* The properties are not deeply reactive, meaning that changes to nested properties will not be detected, and the properties cannot be destructured.
*
* @param initial The initial value of the state.
* @param delay Time until a change until a change to `instant` is propagated to `debounced` (milliseconds)
* @returns A state with `debounced` and `instant` raw reactive properties
*/
export function debounced<T>(initial: T, delay: number = 1000): DebouncedState<T> {
let instant = $state.raw(initial)
let debounced = $state.raw(initial)
let timeout: ReturnType<typeof setTimeout> | undefined
return {
get instant() {
return instant
},
get debounced() {
return debounced
},
set instant(value) {
clearTimeout(timeout)
instant = value
timeout = setTimeout(() => (debounced = value), delay)
},
set debounced(value) {
clearTimeout(timeout)
debounced = value
instant = value
},
}
} It's basically the same solution with stores, except that you cannot assign to properties, which I guess was a strong point, but also a bit of a gimmick. |
You don't need effects for this, I'll see if I can write you an example |
See my solution above, it's the closest thing to the original stores solution. I was wondering if it's possible to make a version that's deeply reactive, but that seems impossible. |
It is and it's quite easy with proxies... I'll show an example when I'm free |
Since i'm "listening" for the state i do need the effect dont i? But at that point i also know that im in an effect due to My goal was to get as close as possible to the behavior with stores. |
I believe that I've figured out exactly what it is that I want: a way to tap into Svelte's deep proxies. Something like |
Which is exactly what you should build with a simple reactive proxy...no need of new runes |
https://svelte.dev/playground/1a6cdcde882b484e846f31676f908e8a?version=5.17.3 Here's the example 😁 |
I see... that's a nice solution, but can get more complex the more you want from it. If Svelte offerred a way to tap into the deep proxy of a state, this would be much easier, more efficient, and work with everything that state already works with (array push/other functions, custom classes with state, SvelteMap...). Not quite sure how that might look, probably not proxy-style |
The core issue here is that |
That's a good point, and probably what I'll end up using. I would've loved if there was a way to hijack the new deep reactivity and be able to obtain the old |
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:
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:Now, I've been trying to convert this to Svelte 5, without too much success. My requirements are:
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:
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:
This might work, but I have so many questions:
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
The text was updated successfully, but these errors were encountered: