Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mean-dryers-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: access last safe value of prop on unmount
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export function CallExpression(node, context) {
e.bindable_invalid_location(node);
}

// We need context in case the bound prop is stale
context.state.analysis.needs_context = true;

break;

case '$host':
Expand Down
3 changes: 3 additions & 0 deletions packages/svelte/src/internal/client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ export const EFFECT_HAS_DERIVED = 1 << 20;
export const STATE_SYMBOL = Symbol('$state');
export const STATE_SYMBOL_METADATA = Symbol('$state metadata');
export const LEGACY_PROPS = Symbol('legacy props');
export const TEARDOWN_PROPS = Symbol('teardown props');
export const LOADING_ATTR_SYMBOL = Symbol('');

export const CTX_DESTROYED = 1 << 1;
48 changes: 42 additions & 6 deletions packages/svelte/src/internal/client/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ 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_DESTROYED, TEARDOWN_PROPS } from './constants.js';
import { define_property } from '../shared/utils.js';

/** @type {ComponentContext | null} */
export let component_context = null;
Expand Down Expand Up @@ -112,15 +114,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 = {
Expand All @@ -131,6 +135,31 @@ export function push(props, runes = false, fn) {
};
}

teardown(() => {
if ((ctx.f & CTX_DESTROYED) !== 0) {
return;
}
// Mark the context as destroyed, so any derived props can use
// the latest known value before teardown
ctx.f ^= CTX_DESTROYED;

// Only apply the latest known props before teardown in legacy mode
if (!is_runes(ctx)) {
var teardown_props = ctx.tp;
if (TEARDOWN_PROPS in props) {
props[TEARDOWN_PROPS] = teardown_props;
return;
}
// Apply the latest known props before teardown over existing props
for (var key in teardown_props) {
define_property(props, key, {
value: teardown_props[key],
configurable: true
});
}
}
});

if (DEV) {
// component function
component_context.function = fn;
Expand Down Expand Up @@ -171,15 +200,22 @@ export function pop(component) {
dev_current_component_function = context_stack_item.p?.function ?? null;
}
context_stack_item.m = true;

// Only apply the latest known props before teardown in legacy mode
if (!is_runes(context_stack_item)) {
effect(() => {
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
return component || /** @type {T} */ ({});
}

/** @returns {boolean} */
export function is_runes() {
return !legacy_mode_flag || (component_context !== null && component_context.l === null);
export function is_runes(ctx = component_context) {
return !legacy_mode_flag || (ctx !== null && ctx.l === null);
}

/**
Expand Down
50 changes: 37 additions & 13 deletions packages/svelte/src/internal/client/reactivity/props.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @import { Source } from './types.js' */
/** @import { Derived, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
Expand All @@ -10,27 +10,20 @@ import {
import { get_descriptor, is_function } from '../../shared/utils.js';
import { mutable_source, set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
import {
active_effect,
get,
captured_signals,
set_active_effect,
untrack,
active_reaction,
set_active_reaction
} from '../runtime.js';
import { get, captured_signals, untrack } from '../runtime.js';
import { safe_equals } from './equality.js';
import * as e from '../errors.js';
import {
BRANCH_EFFECT,
CTX_DESTROYED,
LEGACY_DERIVED_PROP,
LEGACY_PROPS,
ROOT_EFFECT,
STATE_SYMBOL
STATE_SYMBOL,
TEARDOWN_PROPS
} from '../constants.js';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
import { is_runes } from '../context.js';

/**
* @param {((value?: number) => number)} fn
Expand Down Expand Up @@ -186,6 +179,12 @@ const spread_props_handler = {
}
},
set(target, key, value) {
// If the spread props have been torn down, then replace the existing props with
// the stale props from the teardown
if (key === TEARDOWN_PROPS) {
target.props = [value];
return true;
}
let i = target.props.length;
while (i--) {
let p = target.props[i];
Expand Down Expand Up @@ -216,6 +215,9 @@ const spread_props_handler = {
}
},
has(target, key) {
if (key === TEARDOWN_PROPS) {
return true;
}
// To prevent a false positive `is_entry_props` in the `prop` function
if (key === STATE_SYMBOL || key === LEGACY_PROPS) return false;

Expand Down Expand Up @@ -249,6 +251,14 @@ export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
}

/**
* @param {Derived} signal
* @returns {boolean}
*/
function in_destroyed_context(signal) {
return signal.ctx !== null && (signal.ctx.f & CTX_DESTROYED) !== 0;
}

/**
* This function is responsible for synchronizing a possibly bound prop with the inner component state.
* It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value.
Expand Down Expand Up @@ -382,6 +392,11 @@ export function prop(props, key, flags, fallback) {
return (inner_current_value.v = parent_value);
});

// Read the bindable prop eagerly to ensure it has a value
if (!is_runes() && bindable) {
get(current_value);
}

if (!immutable) current_value.equals = safe_equals;

return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
Expand All @@ -408,11 +423,20 @@ export function prop(props, key, flags, fallback) {
if (fallback_used && fallback_value !== undefined) {
fallback_value = new_value;
}
if (in_destroyed_context(current_value)) {
return value;
}
untrack(() => get(current_value)); // force a synchronisation immediately
}

return value;
}

// If the prop is read, we might need to return the stale value if component ctx has been destroyed
if (in_destroyed_context(current_value)) {
return current_value.v;
}

return get(current_value);
};
}
9 changes: 5 additions & 4 deletions packages/svelte/src/internal/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ export type ComponentContext = {
effect: null | Effect;
reaction: null | Reaction;
}>;
/** ctx flags */
f: number;
/** mounted */
m: boolean;
/**
* props — needed for legacy mode lifecycle functions, and for `createEventDispatcher`
* @deprecated remove in 6.0
*/
/** props — needed for legacy mode lifecycle functions, for `createEventDispatcher` and teardown */
s: Record<string, unknown>;
/**
* exports (and props, if `accessors: true`) — needed for `createEventDispatcher`
Expand Down Expand Up @@ -53,6 +52,8 @@ export type ComponentContext = {
/** This tracks whether `$:` statements have run in the current cycle, to ensure they only run once */
r2: Source<boolean>;
};
/** teardown props */
tp: Record<string, unknown>;
/**
* dev mode only: the component function
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
import { onDestroy } from 'svelte';

export let my_prop;

onDestroy(() => {
console.log(my_prop.foo);
});
</script>

{my_prop.foo}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { test } from '../../test';
import { flushSync } from 'svelte';

export default test({
async test({ assert, target, logs }) {
const [btn1] = target.querySelectorAll('button');

flushSync(() => {
btn1.click();
});

assert.deepEqual(logs, ['bar']);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
import Component from './Component.svelte';

let value = { foo: 'bar' };
</script>

<button
onclick={() => {
value = undefined;
}}>Reset value</button
>

{#if value !== undefined}
<Component my_prop={value} />
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts">
export let ref;
</script>

<input bind:this={ref} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { test } from '../../test';
import { flushSync } from 'svelte';

export default test({
async test({ assert, target, logs }) {
const [btn1] = target.querySelectorAll('button');

btn1.click();
flushSync();
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script>
import Component from './Component.svelte';

let state = { title: 'foo' };
</script>

{#if state}
{@const attributes = { title: state.title }}
<Component {...attributes} />
{/if}
<button
onclick={() => {
state = undefined;
}}
>
Del
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
import { onDestroy } from "svelte";

export let checked;
export let count;

onDestroy(() => {
console.log(count, checked);
});
</script>

<p>{count}</p>

<button onclick={()=> count-- }></button>
Loading