svelte
svelte copied to clipboard
Feat: `$effect.async`, `$derived.async`
Describe the problem
There have been several discussions about the issues of runes and async, for example, #13916. Many apps depend on async code, and it can become difficult to migrate to Svelte 5 because of this.
Describe the proposed solution
It would be useful to have two asynchronous runes: $effect.async and $derived.async. These functions would be asynchronous counterparts to $effect and $derived.by, respectively. Here's an example:
let fetched = $derived.async(async()=>{
let res = await fetch('./example.json');
return await res.json();
});
$effect.async(async () =>{
let asyncValue = await getAsyncValue();
if(asyncValue === preferredAsyncValue) {
runCallback();
}
});
You may notice that these functions do not require await to be called to use them, this would be because the await would be inserted at compile time:
let thing = $derived.async(asyncValue);
$inspect(thing);
//turns into
let thing = await $.derived_async(asyncValue);
$.inspect_async(async() =>[await $.get_async(thing)]);
Importance
would make my life easier
And the expectation here would be for all reactive signals to account for effect re-run and derived value recalculation, even the ones read after awaiting? Otherwise it would be identical to the current $effect and $derived, in which case it would be simpler to just bring back the asynchronous typing for them (well, at least $effect once allowed async delegates).
The asynchronous effects and deriveds would run on a separate schedule from the regular effects and deriveds. This would have similar internal functionality to the regular scheduler, and could probably use the Scheduler API if needed, but that doesn't have full browser support. This would be on a separate schedule to not slow down regular effects and deriveds (which would otherwise have to wait for asynchronous, and more time-consuming functions)
This would be on a separate schedule to not slow down regular effects and deriveds (which would otherwise have to wait for asynchronous, and more time-consuming functions)
if you update the typings to accept an async function (btw i think we should not do such thing) but don't await the function when running the effect they would not wait for it.
if you update the typings to accept an async function (btw i think we should not do such thing) but don't await the function when running the effect they would not wait for it.
Ah, I see. Is there a reason why you're against the type updates?
if you update the typings to accept an async function (btw i think we should not do such thing) but don't await the function when running the effect they would not wait for it.
I understand the core team's reasoning behind this, but it is counter-productive.
Let me explain.
Fact: Synchronous code inside effects is easier to understand and troubleshoot. It is simpler and more straightforward to follow in order to, for example, determine the reactive triggers.
Fact: Real world applications require tons of asynchronous operations triggered inside effects.
So, while the core team's reason behind disallowing async delegates for $effect is super understandable, reality forces us to do the React-like (which I hate) pattern:
$effect(() => {
const x = async () => { ... };
x();
});
// As opposed to allowing async:
$effect(async () => { ... });
Both are the same, work the same. So in the end, the prohibition of async on effect gains, what? In my eyes, the only gain of disallowing async is forcing users to the more verbose and unneeded syntax. Yes, users must also be taught that effect tracking only works up to the first await. I get it.
Ah, I see. Is there a reason why you're against the type updates?
Because async code in effects and derived is very risky. Everything after an await is not tracked and it shouldn't be the simple thing to do. Generally you shouldn't ever use async in both of those but if you really really need you can do it but it's best that you think about it before doing it.
In the future we'll look into proper async primitives, as they will likely require some form of suspense to work correctly. As noted above, the reason why these can be problematic is because of a few reasons:
- in effects, the destroy function is essential in many ways, if you make it async then it simply won't work
- in effects/deriveds we need to track the reactive dependencies, this is only possible sync today in JavaScript because we lack something like
AsyncContext - we can't just add
awaitseverywhere, as they too will need to be in an async context, so it ends up with us now working with promises instead of values – which are problematic to value equality but also things like error handling – if you throw an error then it might get swallowed by accident
Generally speaking, we want to avoid all these caveats entirely. If you have something async, you can call from it inside your synchronous effect. Watch this space too, as in the new year we hope to build out some powerful async primitives that will really help with these use-cases.
in effects, the destroy function is essential in many ways, if you make it async then it simply won't work
Oh yeah I completely forgot about this too
Hello!
Thank you for this great framework and your hard work.
Are there any updates in this field?
In the $effect documentation you state the following:
In general, $effect is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently.
In particular, avoid using it to synchronise state.
Then you recommend to use the $derived function.
However, as my experience shows, very often you DO need to synchronise state using async calls. This is a very common pattern. However, the recommended $derived function doesn't support async calls (at least legally). I believe Svelte needs some async primitives to be introduced or the documentation updated to properly address these very common UI patterns or at least the docs shouldn't recommend to avoid them.
I hope it makes sense. Thanks.
See #15845