fix: access last safe value of prop on unmount

pull/15400/head
Dominic Gannaway 2 years ago
parent be82332ac8
commit d3a69c55a5

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: access last safe value of prop on unmount

@ -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;

@ -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

@ -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);
};
}

@ -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;

@ -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<string, unknown>;
};
export type ComponentContextLegacy = ComponentContext & {

@ -0,0 +1,10 @@
<script>
let { checked = $bindable(), count = $bindable() } = $props();
$effect(() => ()=>{
console.log(count, checked);
});
</script>
<p>{count}</p>
<button onclick={()=> count-- }></button>

@ -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
]);
}
});

@ -0,0 +1,44 @@
<script>
import Component from "./Component.svelte";
let show = $state(true);
let count = $state(0);
let spread = $derived({ checked: show, count });
let Dynamic = $derived(count < 2 ? Component : undefined);
let Dynamic2 = $derived(show ? Component : undefined);
</script>
<button onclick={()=> count++ }></button>
<button onclick={()=> show = !show }></button>
<!-- count with bind -->
{#if count < 2}
<Component bind:count bind:checked={show} />
{/if}
<!-- spread syntax -->
{#if count < 2}
<Component {...spread} />
{/if}
<!-- normal prop -->
{#if count < 2}
<Component {count} checked={show} />
{/if}
<!-- prop only accessed in destroy -->
{#if show}
<Component {count} checked={show} />
{/if}
<!-- dynamic component -->
<Dynamic {count} checked={show} />
<!-- dynamic component spread -->
<Dynamic {...spread} />
<!-- dynamic component with prop only accessed on destroy -->
<Dynamic2 {count} checked={show} />
<!-- dynamic component with prop only accessed on destroy spread -->
<Dynamic2 {...spread} />
Loading…
Cancel
Save