diff --git a/.changeset/mean-dryers-fix.md b/.changeset/mean-dryers-fix.md new file mode 100644 index 0000000000..96037f3440 --- /dev/null +++ b/.changeset/mean-dryers-fix.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: access last safe value of prop on unmount diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index a4840ce4eb..5108bdd2b3 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -25,3 +25,6 @@ export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); + +export const CTX_CONTAINS_TEARDOWN = 1; +export const CTX_DESTROYED = 2; diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index bd94d5ad8a..7ac3981b2a 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -11,8 +11,9 @@ import { set_active_reaction, untrack } from './runtime.js'; -import { effect } from './reactivity/effects.js'; +import { effect, teardown } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; +import { CTX_CONTAINS_TEARDOWN, CTX_DESTROYED } from './constants.js'; /** @type {ComponentContext | null} */ export let component_context = null; @@ -112,15 +113,17 @@ export function getAllContexts() { * @returns {void} */ export function push(props, runes = false, fn) { - component_context = { + var ctx = (component_context = { p: component_context, c: null, e: null, + f: 0, m: false, s: props, x: null, - l: null - }; + l: null, + tp: props + }); if (legacy_mode_flag && !runes) { component_context.l = { @@ -131,6 +134,24 @@ export function push(props, runes = false, fn) { }; } + teardown(() => { + if (ctx.f !== CTX_CONTAINS_TEARDOWN) { + return; + } + // Mark the context as destroyed, so any derived props can use + // the latest known value before teardown + ctx.f = CTX_DESTROYED; + + var teardown_props = ctx.tp; + // Apply the latest known props before teardown over existing props + for (var key in teardown_props) { + Object.defineProperty(props, key, { + value: teardown_props[key], + configurable: true + }); + } + }); + if (DEV) { // component function component_context.function = fn; @@ -171,6 +192,12 @@ export function pop(component) { dev_current_component_function = context_stack_item.p?.function ?? null; } context_stack_item.m = true; + + effect(() => { + if (context_stack_item.f === CTX_CONTAINS_TEARDOWN) { + context_stack_item.tp = { ...context_stack_item.s }; + } + }); } // Micro-optimization: Don't set .a above to the empty object // so it can be garbage-collected when the return here is unused diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 5a3b30281f..fdff79131c 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -23,6 +23,8 @@ import { safe_equals } from './equality.js'; import * as e from '../errors.js'; import { BRANCH_EFFECT, + CTX_DESTROYED, + DESTROYED, LEGACY_DERIVED_PROP, LEGACY_PROPS, ROOT_EFFECT, @@ -31,6 +33,7 @@ import { import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; +import { component_context } from '../context.js'; /** * @param {((value?: number) => number)} fn @@ -369,6 +372,12 @@ export function prop(props, key, flags, fallback) { // source is written to from various places to persist this value. var inner_current_value = mutable_source(prop_value); var current_value = derived(() => { + var ctx = component_context; + + if (ctx !== null && ctx.f === CTX_DESTROYED) { + return get(inner_current_value); + } + var parent_value = getter(); var child_value = get(inner_current_value); @@ -413,6 +422,7 @@ export function prop(props, key, flags, fallback) { return value; } + return get(current_value); }; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 486c819f36..f5abc09aa6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -22,7 +22,8 @@ import { ROOT_EFFECT, LEGACY_DERIVED_PROP, DISCONNECTED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + CTX_CONTAINS_TEARDOWN } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set } from './reactivity/sources.js'; @@ -566,7 +567,14 @@ export function update_effect(effect) { execute_effect_teardown(effect); var teardown = update_reaction(effect); - effect.teardown = typeof teardown === 'function' ? teardown : null; + if (typeof teardown === 'function') { + if (effect.ctx !== null && effect.ctx.f === 0) { + effect.ctx.f = CTX_CONTAINS_TEARDOWN; + } + effect.teardown = teardown; + } else { + effect.teardown = null; + } effect.wv = write_version; var deps = effect.deps; diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 7208ed7783..ac5f6101b4 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -20,6 +20,8 @@ export type ComponentContext = { effect: null | Effect; reaction: null | Reaction; }>; + /** ctx flags */ + f: number; /** mounted */ m: boolean; /** @@ -57,6 +59,8 @@ export type ComponentContext = { * dev mode only: the component function */ function?: any; + /** teardown props */ + tp: Record; }; export type ComponentContextLegacy = ComponentContext & { diff --git a/packages/svelte/tests/runtime-runes/samples/ondestroy-prop-access/Component.svelte b/packages/svelte/tests/runtime-runes/samples/ondestroy-prop-access/Component.svelte new file mode 100644 index 0000000000..afe0fe1087 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ondestroy-prop-access/Component.svelte @@ -0,0 +1,10 @@ + + +

{count}

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/ondestroy-prop-access/_config.js b/packages/svelte/tests/runtime-runes/samples/ondestroy-prop-access/_config.js new file mode 100644 index 0000000000..9b43fa16b9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ondestroy-prop-access/_config.js @@ -0,0 +1,68 @@ +import { ok, test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, logs }) { + const [btn1, btn2, btn3] = target.querySelectorAll('button'); + let ps = [...target.querySelectorAll('p')]; + + for (const p of ps) { + assert.equal(p.innerHTML, '0'); + } + + flushSync(() => { + btn1.click(); + }); + + // prop update normally if we are not unmounting + for (const p of ps) { + assert.equal(p.innerHTML, '1'); + } + + flushSync(() => { + btn3.click(); + }); + + // binding still works and update the value correctly + for (const p of ps) { + assert.equal(p.innerHTML, '0'); + } + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + console.warn(logs); + + // the five components guarded by `count < 2` unmount and log + assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]); + + flushSync(() => { + btn2.click(); + }); + + // the three components guarded by `show` unmount and log + assert.deepEqual(logs, [ + 1, + true, + 1, + true, + 1, + true, + 1, + true, + 1, + true, + 2, + true, + 2, + true, + 2, + false + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/ondestroy-prop-access/main.svelte b/packages/svelte/tests/runtime-runes/samples/ondestroy-prop-access/main.svelte new file mode 100644 index 0000000000..3e15ac32d4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/ondestroy-prop-access/main.svelte @@ -0,0 +1,44 @@ + + + + + + +{#if count < 2} + +{/if} + + +{#if count < 2} + +{/if} + + +{#if count < 2} + +{/if} + + +{#if show} + +{/if} + + + + + + + + + + + +