fix: re-process batch if new root effects were scheduled (#17895)

In some cases a new branch might create effects which via
reading/writing reschedule an effect, causing `this.#roots` to become
populated again. In this case we need to re-process the batch. Most of
the time this will just result in a cleanup of the dirtied branches
since other work is already handled via running the effects etc. - it's
still crucial, else the reactive graph becomes frozen since no new root
effects are scheduled.

Fixes #17891

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/17896/head
Simon H 2 months ago committed by GitHub
parent 51d305d18a
commit 342d8568f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: re-process batch if new root effects were scheduled

@ -271,6 +271,14 @@ export class Batch {
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
// Edge case: During traversal new branches might create effects that run immediately and set state,
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// once more in that case - most of the time this will just clean up dirty branches.
if (this.#roots.length > 0) {
const batch = (next_batch ??= this);
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
}
if (next_batch !== null) {
batches.add(next_batch);

@ -0,0 +1,46 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [open, close, increment] = target.querySelectorAll('button');
open.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>0</button>
<div>open (width: 42)</div>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>open (width: 42)</div>
`
);
close.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>closed</div>
`
);
assert.deepEqual(logs, ['effect ran']);
}
});

@ -0,0 +1,37 @@
<script module>
let active = $state(false);
let panelWidth = $state(null);
const store = {
get active() { return active; },
open() { active = true; },
close() { active = false; },
// This getter lazily writes $state on first read
get panelWidth() {
if (panelWidth === null) panelWidth = 42;
$effect(() => {
console.log('effect ran');
});
return panelWidth;
}
};
</script>
<script>
let counter = $state(0);
</script>
<button onclick={() => store.open()}>Open</button>
<button onclick={() => store.close()}>Close</button>
<button onclick={() => counter++}>{counter}</button>
<div>
{#if store.active}
open (width: {store.panelWidth})
{:else}
closed
{/if}
</div>

@ -0,0 +1,44 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [open, close, increment] = target.querySelectorAll('button');
open.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>0</button>
<div>open (width: 42)</div>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>open (width: 42)</div>
`
);
close.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>closed</div>
`
);
}
});

@ -0,0 +1,31 @@
<script module>
let active = $state(false);
let panelWidth = $state(null);
const store = {
get active() { return active; },
open() { active = true; },
close() { active = false; },
// This getter lazily writes $state on first read
get panelWidth() {
if (panelWidth === null) panelWidth = 42;
return panelWidth;
}
};
</script>
<script>
let counter = $state(0);
</script>
<button onclick={() => store.open()}>Open</button>
<button onclick={() => store.close()}>Close</button>
<button onclick={() => counter++}>{counter}</button>
<div>
{#if store.active}
open (width: {store.panelWidth})
{:else}
closed
{/if}
</div>
Loading…
Cancel
Save