fix: avoid unnecessary block effect re-runs after async work completes (#17535)

* add samples for async readable freeze

* test updating without changing reference too

* fix: treat block effects as maybe dirty in revive()

* even simpler - put block effects in maybe_dirty_effects

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/14594/merge
David 3 days ago committed by GitHub
parent e1427aa0c8
commit fe12ffc2a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid unnecessary block effect re-runs after async work completes

@ -243,7 +243,7 @@ export class Batch {
} else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) {
render_effects.push(effect);
} else if (is_dirty(effect)) {
if ((flags & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect);
if ((flags & BLOCK_EFFECT) !== 0) this.#maybe_dirty_effects.add(effect);
update_effect(effect);
}

@ -0,0 +1,24 @@
import { tick } from 'svelte';
import { test } from '../../test';
// ensure in-place object mutations stay reactive in async
export default test({
skip_no_async: true,
async test({ assert, target }) {
const button = /** @type {HTMLElement} */ (target.querySelector('button'));
await tick();
assert.htmlEqual(target.innerHTML, `<p>count: 1, computed: 10</p><button>mutate</button>`);
button.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>count: 2, computed: 20</p><button>mutate</button>`);
button.click();
await tick();
assert.htmlEqual(target.innerHTML, `<p>count: 3, computed: 30</p><button>mutate</button>`);
}
});

@ -0,0 +1,26 @@
<script>
import { writable } from 'svelte/store';
const data = writable({ count: 1 });
function mutate_count() {
data.update(d => {
d.count++;
return d;
});
}
async function compute(d) {
return Promise.resolve(d.count * 10);
}
</script>
<svelte:boundary>
<p>count: {$data.count}, computed: {await compute($data)}</p>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>
<button onclick={mutate_count}>mutate</button>

@ -0,0 +1,32 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
skip_no_async: true,
async test({ assert, target }) {
const button = /** @type {HTMLElement} */ (target.querySelector('button'));
await tick();
assert.htmlEqual(
target.innerHTML,
`<ul><li data-item="1"><span>10</span><span>20</span></li></ul><button>add</button>`
);
button.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<ul><li data-item="1"><span>10</span><span>20</span></li><li data-item="2"><span>20</span><span>40</span></li></ul><button>add</button>`
);
button.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<ul><li data-item="1"><span>10</span><span>20</span></li><li data-item="2"><span>20</span><span>40</span></li><li data-item="3"><span>30</span><span>60</span></li></ul><button>add</button>`
);
}
});

@ -0,0 +1,31 @@
<script>
import { writable } from 'svelte/store';
const items = writable([{ id: 1 }]);
function add_item() {
items.update(arr => [...arr, { id: arr.length + 1 }]);
}
function query(item) {
return Promise.resolve([item.id * 10, item.id * 20]);
}
</script>
<svelte:boundary>
<ul>
{#each $items as item (item.id)}
<li data-item={item.id}>
{#each await query(item) as value}
<span>{value}</span>
{/each}
</li>
{/each}
</ul>
{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>
<button onclick={add_item}>add</button>
Loading…
Cancel
Save