From dc1f037fa67bf2fbdeb1c5f18b2b1f7d023077e3 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 18 May 2026 17:37:01 +0200 Subject: [PATCH] fix: don't run teardown effects when deriveds are unfreezed (#18227) The logic was flawed - a teardown effect only has a teardown function but not `fn` property, but unfreeze thought that everything with a `teardown` needs to be unfreezed Helps with #18221 (though likely doesn't fix it completely, at least not the more general `batch.#roots`problems) --- .changeset/slimy-olives-cross.md | 5 +++++ .../internal/client/reactivity/deriveds.js | 6 +++--- .../async-state-eager-const/_config.js | 21 +++++++++++++++++++ .../async-state-eager-const/main.svelte | 21 +++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 .changeset/slimy-olives-cross.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte diff --git a/.changeset/slimy-olives-cross.md b/.changeset/slimy-olives-cross.md new file mode 100644 index 0000000000..3309e2deab --- /dev/null +++ b/.changeset/slimy-olives-cross.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't run teardown effects when deriveds are unfreezed diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 75be018552..8d240bca31 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -452,8 +452,8 @@ export function freeze_derived_effects(derived) { // make it a noop so it doesn't get called again if the derived // is unfrozen. we don't set it to `null`, because the existence // of a teardown function is what determines whether the - // effect runs again during unfreezing - e.teardown = noop; + // effect runs again during unfreezing (but not for teardown-only effects) + if (e.fn !== null) e.teardown = noop; e.ac = null; remove_reactions(e, 0); @@ -471,7 +471,7 @@ export function unfreeze_derived_effects(derived) { for (const e of derived.effects) { // if the effect was previously frozen — indicated by the presence // of a teardown function — unfreeze it - if (e.teardown) { + if (e.teardown && e.fn !== null) { update_effect(e); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js new file mode 100644 index 0000000000..1511d443e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js @@ -0,0 +1,21 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { dev: true }, // for testing that teardown effect in eager $.get(loaded) doesn't lead to a crash (because it means REACTION_RAN is set, which means unfreeze_derived runs) + async test({ assert, target }) { + const [count, shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `
0
`); + + count.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `0 (...)
`); + + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `1
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte new file mode 100644 index 0000000000..f0e2f1cdac --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte @@ -0,0 +1,21 @@ + + + + + +{await push(count)} {loaded ? '' : '(...)'}
+ + {#snippet pending()}{/snippet} +