diff --git a/.changeset/rare-ducks-sip.md b/.changeset/rare-ducks-sip.md new file mode 100644 index 0000000000..3575e9f726 --- /dev/null +++ b/.changeset/rare-ducks-sip.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip derived re-evaluation inside destroyed branch effects diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5da0df0670..dcc92d55e0 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -12,7 +12,9 @@ import { WAS_MARKED, DESTROYED, CLEAN, - REACTION_RAN + INERT, + REACTION_RAN, + BRANCH_EFFECT } from '#client/constants'; import { active_reaction, @@ -40,7 +42,7 @@ import { get_error } from '../../shared/dev.js'; import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { batch_values, current_batch } from './batch.js'; +import { batch_values, collected_effects, current_batch } from './batch.js'; import { increment_pending, unset_context } from './async.js'; import { deferred, includes, noop } from '../../shared/utils.js'; import { set_signal_status, update_derived_status } from './status.js'; @@ -328,9 +330,7 @@ function get_derived_parent_effect(derived) { var parent = derived.parent; while (parent !== null) { if ((parent.f & DERIVED) === 0) { - // The original parent effect might've been destroyed but the derived - // is used elsewhere now - do not return the destroyed effect in that case - return (parent.f & DESTROYED) === 0 ? /** @type {Effect} */ (parent) : null; + return /** @type {Effect} */ (parent); } parent = parent.parent; } @@ -343,10 +343,24 @@ function get_derived_parent_effect(derived) { * @returns {T} */ export function execute_derived(derived) { + var raw_parent = get_derived_parent_effect(derived); + var parent_effect = raw_parent !== null && (raw_parent.f & DESTROYED) !== 0 ? null : raw_parent; + + // don't update deriveds inside a destroyed branch (e.g. {#if} or {#each}) — + // the branch scope is invalid and evaluating could trigger side effects + // with stale values. + if ( + !is_destroying_effect && + raw_parent !== null && + (raw_parent.f & (DESTROYED | BRANCH_EFFECT)) === (DESTROYED | BRANCH_EFFECT) + ) { + return derived.v; + } + var value; var prev_active_effect = active_effect; - set_active_effect(get_derived_parent_effect(derived)); + set_active_effect(parent_effect); if (DEV) { let prev_eager_effects = eager_effects; @@ -384,6 +398,32 @@ export function execute_derived(derived) { * @returns {void} */ export function update_derived(derived) { + // Don't re-evaluate deriveds inside INERT (outroing) branches when the + // read originates from outside the branch. Re-evaluating would use stale + // dependency values (e.g. a prop that became `undefined` when the branch + // condition changed), violating the `{#if}` contract. + // + // In non-async mode, INERT branches are never walked by the scheduler, + // so any read is necessarily external — block unconditionally. + // + // In async mode, INERT branches ARE walked (to keep transitions alive), + // so we only block reads during effect flushing (collected_effects === null + // and active_effect === null), which indicates the reader is an external + // effect, not the branch's own traversal. + if (!is_destroying_effect) { + var dominated_by_inert = async_mode_flag + ? collected_effects === null && active_effect === null + : true; + + if (dominated_by_inert) { + var parent = get_derived_parent_effect(derived); + + if (parent !== null && (parent.f & INERT) !== 0 && (parent.f & DESTROYED) === 0) { + return; + } + } + } + var old_value = derived.v; var value = execute_derived(derived); diff --git a/packages/svelte/tests/runtime-runes/samples/if-block-derived-inert-external-reader/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/if-block-derived-inert-external-reader/Inner.svelte new file mode 100644 index 0000000000..e9e6e0d18d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/if-block-derived-inert-external-reader/Inner.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/if-block-derived-inert-external-reader/_config.js b/packages/svelte/tests/runtime-runes/samples/if-block-derived-inert-external-reader/_config.js new file mode 100644 index 0000000000..ee98fbfcff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/if-block-derived-inert-external-reader/_config.js @@ -0,0 +1,36 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// Covers the INERT (outroing) counterpart to if-block-const-destroyed-external-reader. +// An external $derived reads a child component's $derived via bind:this, keeping it +// connected in the reactive graph while the branch is outroing. Without a guard, +// the inner derived re-evaluates with stale values mid-transition and crashes. +// The fix returns the cached value and keeps the derived dirty so it re-evaluates +// correctly if the branch reverses (INERT cleared) rather than being stuck with +// a stale clean value. +export default test({ + ssrHtml: '
', + html: 'HELLO
', + + async test({ assert, raf, target }) { + const [button] = target.querySelectorAll('button'); + + // Clearing value starts the out-transition (branch becomes INERT). + // Without the guard this crashes with a TypeError in async mode. + flushSync(() => button.click()); + + assert.htmlEqual( + target.innerHTML, + 'HELLO
' + ); + + // Complete the transition — branch is now destroyed and div is removed. + raf.tick(100); + + // Flush the bind:this teardown microtask and resulting effect updates. + await Promise.resolve(); + flushSync(); + + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/if-block-derived-inert-external-reader/main.svelte b/packages/svelte/tests/runtime-runes/samples/if-block-derived-inert-external-reader/main.svelte new file mode 100644 index 0000000000..f66ddb7015 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/if-block-derived-inert-external-reader/main.svelte @@ -0,0 +1,21 @@ + + +{#if value} +{externalView}