Skip to content

Commit f549478

Browse files
hmnddummdidumm
andauthored
fix: guard contents updated before the guard itself (#16930)
Fixes #16850, fixes #16775, fixes #16795, fixes #16982 #16631 introduced a bug that results in the effects within guards being evaluated before the guards themselves. This fix makes sure to iterate the block effects in the correct order (top down) --------- Co-authored-by: Simon H <[email protected]> Co-authored-by: Simon Holthausen <[email protected]>
1 parent 7d9ea8e commit f549478

File tree

10 files changed

+137
-7
lines changed

10 files changed

+137
-7
lines changed

.changeset/witty-seas-learn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: ensure guards (eg. if, each, key) run before their contents

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ function infinite_loop_guard() {
561561
}
562562
}
563563

564-
/** @type {Effect[] | null} */
564+
/** @type {Set<Effect> | null} */
565565
export let eager_block_effects = null;
566566

567567
/**
@@ -578,7 +578,7 @@ function flush_queued_effects(effects) {
578578
var effect = effects[i++];
579579

580580
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
581-
eager_block_effects = [];
581+
eager_block_effects = new Set();
582582

583583
update_effect(effect);
584584

@@ -601,15 +601,34 @@ function flush_queued_effects(effects) {
601601

602602
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
603603
// which already handled this logic and did set eager_block_effects to null.
604-
if (eager_block_effects?.length > 0) {
605-
// TODO this feels incorrect! it gets the tests passing
604+
if (eager_block_effects?.size > 0) {
606605
old_values.clear();
607606

608607
for (const e of eager_block_effects) {
609-
update_effect(e);
608+
// Skip eager effects that have already been unmounted
609+
if ((e.f & (DESTROYED | INERT)) !== 0) continue;
610+
611+
// Run effects in order from ancestor to descendant, else we could run into nullpointers
612+
/** @type {Effect[]} */
613+
const ordered_effects = [e];
614+
let ancestor = e.parent;
615+
while (ancestor !== null) {
616+
if (eager_block_effects.has(ancestor)) {
617+
eager_block_effects.delete(ancestor);
618+
ordered_effects.push(ancestor);
619+
}
620+
ancestor = ancestor.parent;
621+
}
622+
623+
for (let j = ordered_effects.length - 1; j >= 0; j--) {
624+
const e = ordered_effects[j];
625+
// Skip eager effects that have already been unmounted
626+
if ((e.f & (DESTROYED | INERT)) !== 0) continue;
627+
update_effect(e);
628+
}
610629
}
611630

612-
eager_block_effects = [];
631+
eager_block_effects.clear();
613632
}
614633
}
615634
}

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ function mark_reactions(signal, status) {
336336
} else if (not_dirty) {
337337
if ((flags & BLOCK_EFFECT) !== 0) {
338338
if (eager_block_effects !== null) {
339-
eager_block_effects.push(/** @type {Effect} */ (reaction));
339+
eager_block_effects.add(/** @type {Effect} */ (reaction));
340340
}
341341
}
342342

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { test } from '../../test';
2+
import { flushSync } from 'svelte';
3+
4+
export default test({
5+
mode: ['client'],
6+
async test({ target, assert, logs }) {
7+
const button = target.querySelector('button');
8+
9+
button?.click();
10+
flushSync();
11+
button?.click();
12+
flushSync();
13+
button?.click();
14+
flushSync();
15+
button?.click();
16+
flushSync();
17+
18+
assert.deepEqual(logs, ['two', 'one', 'two', 'one', 'two']);
19+
}
20+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script lang="ts">
2+
let b = $state(false);
3+
let v = $state("two");
4+
5+
$effect(() => {
6+
v = b ? "one" : "two";
7+
})
8+
</script>
9+
10+
<button onclick={() => b = !b}>Trigger</button>
11+
12+
{#if v === "one"}
13+
<div>if1 matched! {console.log('one')}</div>
14+
{:else if v === "two"}
15+
<div>if2 matched! {console.log('two')}</div>
16+
{:else}
17+
<div>nothing matched {console.log('else')}</div>
18+
{/if}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { test } from '../../test';
2+
import { flushSync } from 'svelte';
3+
4+
export default test({
5+
mode: ['client'],
6+
async test({ target, assert }) {
7+
const button = target.querySelector('button');
8+
9+
flushSync(() => button?.click());
10+
11+
assert.equal(target.textContent?.trim(), 'Trigger');
12+
}
13+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script>
2+
let centerRow = $state({ nested: { optional: 2, required: 3 } });
3+
4+
let someChange = $state(false);
5+
$effect(() => {
6+
if (someChange) centerRow = undefined;
7+
});
8+
</script>
9+
10+
{#if centerRow?.nested}
11+
{#if centerRow?.nested?.optional != undefined && centerRow.nested.optional > 0}
12+
op: {centerRow.nested.optional}<br />
13+
{:else}
14+
req: {centerRow.nested.required}<br />
15+
{/if}
16+
{/if}
17+
18+
<button onclick={() => (someChange = true)}>Trigger</button>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
let { p } = $props();
3+
$effect.pre(() => {
4+
console.log('running ' + p)
5+
})
6+
</script>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
mode: ['client'],
6+
async test({ assert, target, logs }) {
7+
const button = target.querySelector('button');
8+
9+
button?.click();
10+
flushSync();
11+
assert.deepEqual(logs, ['pre', 'running b', 'pre', 'pre']);
12+
}
13+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script>
2+
import Component from './Component.svelte';
3+
4+
let p = $state('b');
5+
6+
$effect.pre(() => {
7+
console.log('pre')
8+
if (p === 'a') p = null;
9+
})
10+
</script>
11+
12+
{#if p || !p}
13+
{#if p}
14+
<Component {p} />
15+
{/if}
16+
{/if}
17+
18+
<button onclick={() => p = 'a'}>a</button>

0 commit comments

Comments
 (0)