fix: ensure $derived returns pre-change value during teardown in eager block effects path

Remove premature `old_values.clear()` in the eager block effects path of
`flush_queued_effects`. This clear ran before eager effects executed their
teardowns, causing $derived reads to return post-change values instead of
pre-change values in $effect.pre and {@attach} cleanups.

Fixes #18391
pull/18402/head
sijie-Z 3 months ago
parent a9f48540e2
commit fa0524da98
No known key found for this signature in database
GPG Key ID: 94BCBD8B2A7E773E

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure `$derived` returns pre-change value during teardown in eager block effects path

@ -1111,8 +1111,6 @@ function flush_queued_effects(effects) {
// If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(), // If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(),
// which already handled this logic and did set eager_block_effects to null. // which already handled this logic and did set eager_block_effects to null.
if (eager_block_effects?.size > 0) { if (eager_block_effects?.size > 0) {
old_values.clear();
for (const e of eager_block_effects) { for (const e of eager_block_effects) {
// Skip eager effects that have already been unmounted // Skip eager effects that have already been unmounted
if ((e.f & (DESTROYED | INERT)) !== 0) continue; if ((e.f & (DESTROYED | INERT)) !== 0) continue;

@ -0,0 +1,25 @@
<script>
import { onDestroy } from 'svelte';
let { count } = $props();
let a = $derived(count * 2);
let b = $derived(count * 2);
let c = $derived(count * 2);
let d = $derived(count * 2);
$effect.pre(() => () => console.log('effect.pre', a));
function attachment(_node) {
return () => console.log('attach', b);
}
$effect(() => () => console.log('effect', c));
onDestroy(() => console.log('onDestroy', d));
</script>
<span {@attach attachment}>a: {a}</span>
<span>b: {b}</span>
<span>c: {c}</span>
<span>d: {d}</span>

@ -0,0 +1,14 @@
import { flushSync } from 'svelte';
import { test, ok } from '../../test';
export default test({
async test({ assert, logs, target }) {
const button = target.querySelector('button');
ok(button);
flushSync(() => button.click());
// All four teardowns should see the pre-change derived value (2), not the post-change value (10)
assert.deepEqual(logs, ['effect.pre', 2, 'attach', 2, 'effect', 2, 'onDestroy', 2]);
}
});

@ -0,0 +1,17 @@
<script>
import Child from './Child.svelte';
let source = $state(1);
let count = $state(1);
// State is updated indirectly via an effect — triggers the eager block effects path
$effect(() => {
count = source;
});
</script>
<button onclick={() => (source = 5)}>unmount</button>
{#if count < 5}
<Child {count} />
{/if}

@ -2,6 +2,6 @@ import { test } from '../../test';
export default test({ export default test({
async test({ assert, logs }) { async test({ assert, logs }) {
assert.deepEqual(logs, [1]); assert.deepEqual(logs, [0]);
} }
}); });

Loading…
Cancel
Save