Open qupig opened 2 months ago
you await push, once it is pushed, then ui rerender which await again...
you await push, once it is pushed, then ui rerender which await again...
Why? I don't see any logical connection to this.
Why does updating $state re-render the await block?
When did await blocks become stateful?
Yeah, there's nothing wrong with it IMO and the Svelte 4 version works fine. If you use the migrate button on the REPL it produces the code in the OP, along with the error.
<script>
let items = [1,2,3]
async function init() {
items.push(4);
}
</script>
{#await init()}
loading...
{:then}
{#each items as item}
<p>{item}</p>
{/each}
{/await}
#await
blocks are triggered again if the value being awaited is changed.
The same is true in Svelte 4, the difference in Svelte 5 is that reactivity crosses function boundaries by default.
This is essentially running items.push
in an effect which produces a loop because the property access (items.push
) and possibly internal access of length
causes a dependency on items
while at the same time items
is being modified.
@brunnerh
#await
blocks are triggered again if the value being awaited is changed.
To be honest, it is difficult to understand this sentence and it is not intuitive. But in order to understand as much as possible, I quickly wrote the following example:
<script>
let obj = {a:1};
async function init() {
console.log("init");
obj = {a:2};
return obj;
}
</script>
<button on:click={()=>(obj={a:3})}>click</button>
{#await init()}
loading...
{:then}
<p>{obj.a}</p>
{/await}
It turns out that the example works as you said in Svelte-5-REPL. But NOT Svelte-4-REPL (Please paste the code yourself).
The same is true in Svelte 4
So, could you please make an example to illustrate this, I can't imagine.
And there is no description of this in either the Svelte4 or Svelte5 documentation.
This approach is NOT intuitive anyway.
I never thought that the await function might be executed multiple times, and there is no return value at all. It's normal logic to manipulate and initialize state in an await function, and it's weird to prevent this. And can people really realize that the await block is actually a $effect? Anyway, I won't.
It does work if you do something like this: REPL
@henrikvilhelmberglund Thank you. Another ugly work around in Svelte 5.
And can people really realize that the await block is actually a $effect? Anyway, I won't.
Everything in the markup is basically an $effect. Same holds true for e.g. if or each blocks
It turns out that the example works as you said in Svelte-5-REPL. But NOT Svelte-4-REPL (Please paste the code yourself).
As I already noted, the behavior is different because reactivity does not cross function boundaries in Svelte 4. If you change the code to actually account for that, you also run into an infinite loop in Svelte 4 but it is not caught (and it just crashed my browser).
To make the function fully reactive, it needs to be declared reactively:
- async function init() {
+ $: init = async () => {
A more simple and common example in Svelte 4 would be a promise variable that is just updated later (e.g. when paging).
let index = 0;
$: promise = loadPage(index);
{#await promise then page}
...
{/await}
<button on:click={() => index++}>Next</button>
You can also avoid the issue in Svelte 5 by using untrack
if you read and write the same state.
untrack(() => items.push(4));
The example is also not very representative since everything after the first await
will not be tracked anyway. This will not cause a loop:
async function init() {
await Promise.resolve(); // e.g. fetch here
items.push(4);
}
And often you will just assign a state and not mutate (causing a read first) in load functions like this.
@brunnerh Great presentation! Thanks!
since everything after the first await will not be tracked anyway
This explains why I ran into the issue when I temporarily removed the async api code to simulate locally.
@MotionlessTrain
Everything in the markup is basically an $effect. Same holds true for e.g. if or each blocks
For if
or each
this is easy to understand, but not await
. But it looks like I need to understand it better.
Everything is working as it should — having side-effects like items.push(...)
in your template is no different than doing something obviously wrong like this:
<script>
let count = $state(0);
</script>
<p>{count++}</p>
For me the only question to me is 'how could we have made this easier to diagnose?'. The current message...
Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
...seems okay but clearly it could be improved. What if it gave more clarity as to where the loop was happening and explained that it happens when you read and write the same value? Like this, perhaps:
An
{#await ...}
block in Foo.svelte updated the value ofitems
, which it also reads from, causing an infinite loop
It would take some finagling (we would need to add labels to sources in dev mode, for example) but it seems doable
@Rich-Harris For me, @brunnerh‘s examples and explanations help me understand more easily what's going on in specific Logic blocks.
Of course, smarter error messages are always better at helping people determine how the error occurred.
The issue itself no longer exists, unless you need it to be tracked to improve the documentation or other parts, please feel free to close it.
Thank you all again for creating everything and explaining the difficult parts so well.
We can keep it open to track the improved error message
Describe the bug
I don't understand why the infinite loop happens.
Reproduction
Svelte-5-REPL
Logs
No response
System Info
Severity
blocking an upgrade