-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
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 |
There is a skill issue here, but it was on the part of whichever nincompoops designed stores in the first place. While it might feel like there's an appealing simplicity to the store version (at least, once you've wrapped your head around the looks-simple-at-first-but-is-actually-quite-convoluted store contract, which I suspect you omitted from your 15 minute time estimate), the reality is that it's buggy as all hell. You'd think this <p>debounced: {JSON.stringify($debounced)}</p>
<p>instant: {JSON.stringify($instant)}</p>
<!-- this should not be visible when debouncing is active! -->
{#if $debounced.prop === $instant.prop}
<p>{$debounced.prop} === {$instant.prop}</p>
{/if} Wrong. In Svelte 4, the code that gets generated looks like this... if (/*$debounced*/ ctx[1].prop === /*$instant*/ ctx[0].prop) {
// ...
} ...and since if (dirty & /*$debounced*/ 2 && t0_value !== (t0_value = /*$debounced*/ ctx[1].prop + "")) set_data(t0, t0_value);
if (dirty & /*$instant*/ 1 && t2_value !== (t2_value = /*$instant*/ ctx[0].prop + "")) set_data(t2, t2_value); ...resulting in absurd outcomes like In Svelte 5, we don't use a dirty bitmask, so there are no stale values. But this just means that This is, of course, very very bad. It's the sort of bug that most people can happily ignore for years, but when it bites you it bites you hard. A framework that accepted those issues rather than working to eliminate them would be deeply unserious. Svelte 5 simply forces you to reckon with the footguns involved in dealing with mutable objects. I do think there'd be some value in having a way to detect changes within a state proxy — I've wondered about this sort of thing, which might make people less inclined to build houses of cards around let object = $state(load('my-key') ?? {...}, {
onchange: () => {
save('my-key', object);
}
}); |
@Rich-Harris thanks for the feedback. Although I'm not the OP, I do have a solid need for stores to be around: Micro-frontends. Store reactivity works across MFE's, while signals do not. This happens because the MFE's use different copies of the Svelte runtime. If I may be so bold: Please have this scenario in mind when you plan towards decommissioning stores. Ideally, it would be nice if the Svelte runtime could be externalized, so the resulting MFE-based application can share signal reactivity across projects. For whatever is worth, I'm open to helping with anything I can towards an effort to make MFE's cross-reactive. Thanks! |
Fwiw I did explore this a bit and a new release of em.sh + import maps and externalization should solve the issue (still didn't try tho)...the alternative could be to bundle and serve a svelte version yourself. But I think this is generally a problem of microfrontends right? |
I guess this problem is common in MFE's, yes, because signals usually depend on a central location that orchestrates the change propagation. I suppose that Angular and SolidJS, for instance, would exhibit the same limitation. However, I care not (currently) about any of those because I don't see myself abandoning beautiful Svelte. 😄 I remember that you (Paolo) experimented a bit, and you left me a message to test. I just have had too much of other things to do, and unfortunately did not test. Generally speaking, however, and speaking in favor of stores: It is such a generic contract that is so simple to satisfy that I personally think it would be a shame if they disappeared. I can make anything reactive, even cross-MFE, by providing a |
I guess, after all, the real problem was trying to think in stores, but with state. I'm guessing more people might have done this, since stores were pretty much the standard (and usually only) way to share state in earlier versions, be it global state, contexts, or interacting with regular JS. Now, I would really love to see the |
Since you opened an issue with an actual feature request i'm gonna close this one @rChaoz is that ok? Feel free to reopen if you think there's a reason for it. |
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: