Skip to content

Commit 110d420

Browse files
trueadmRich-Harris
andauthoredMar 11, 2025··
fix: on teardown, use the last known value for the signal before the set (#15469)
* fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * Update packages/svelte/src/internal/client/reactivity/props.js Co-authored-by: Rich Harris <[email protected]> * Update packages/svelte/src/internal/client/reactivity/props.js Co-authored-by: Rich Harris <[email protected]> * Update packages/svelte/src/internal/client/reactivity/props.js Co-authored-by: Rich Harris <[email protected]> * lint * lint * lint * Update .changeset/sharp-elephants-invite.md --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 1cc5bcd commit 110d420

File tree

17 files changed

+254
-31
lines changed

17 files changed

+254
-31
lines changed
 

‎.changeset/sharp-elephants-invite.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
fix: make values consistent between effects and their cleanup functions

‎packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export function CallExpression(node, context) {
4242
e.bindable_invalid_location(node);
4343
}
4444

45+
// We need context in case the bound prop is stale
46+
context.state.analysis.needs_context = true;
47+
4548
break;
4649

4750
case '$host':

‎packages/svelte/src/internal/client/context.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
set_active_reaction,
1212
untrack
1313
} from './runtime.js';
14-
import { effect } from './reactivity/effects.js';
14+
import { effect, teardown } from './reactivity/effects.js';
1515
import { legacy_mode_flag } from '../flags/index.js';
1616

1717
/** @type {ComponentContext | null} */
@@ -112,15 +112,16 @@ export function getAllContexts() {
112112
* @returns {void}
113113
*/
114114
export function push(props, runes = false, fn) {
115-
component_context = {
115+
var ctx = (component_context = {
116116
p: component_context,
117117
c: null,
118+
d: false,
118119
e: null,
119120
m: false,
120121
s: props,
121122
x: null,
122123
l: null
123-
};
124+
});
124125

125126
if (legacy_mode_flag && !runes) {
126127
component_context.l = {
@@ -131,6 +132,10 @@ export function push(props, runes = false, fn) {
131132
};
132133
}
133134

135+
teardown(() => {
136+
/** @type {ComponentContext} */ (ctx).d = true;
137+
});
138+
134139
if (DEV) {
135140
// component function
136141
component_context.function = fn;

‎packages/svelte/src/internal/client/reactivity/props.js

+26-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Source } from './types.js' */
1+
/** @import { Derived, Source } from './types.js' */
22
import { DEV } from 'esm-env';
33
import {
44
PROPS_IS_BINDABLE,
@@ -10,24 +10,10 @@ import {
1010
import { get_descriptor, is_function } from '../../shared/utils.js';
1111
import { mutable_source, set, source, update } from './sources.js';
1212
import { derived, derived_safe_equal } from './deriveds.js';
13-
import {
14-
active_effect,
15-
get,
16-
captured_signals,
17-
set_active_effect,
18-
untrack,
19-
active_reaction,
20-
set_active_reaction
21-
} from '../runtime.js';
13+
import { get, captured_signals, untrack } from '../runtime.js';
2214
import { safe_equals } from './equality.js';
2315
import * as e from '../errors.js';
24-
import {
25-
BRANCH_EFFECT,
26-
LEGACY_DERIVED_PROP,
27-
LEGACY_PROPS,
28-
ROOT_EFFECT,
29-
STATE_SYMBOL
30-
} from '../constants.js';
16+
import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '../constants.js';
3117
import { proxy } from '../proxy.js';
3218
import { capture_store_binding } from './store.js';
3319
import { legacy_mode_flag } from '../../flags/index.js';
@@ -249,6 +235,14 @@ export function spread_props(...props) {
249235
return new Proxy({ props }, spread_props_handler);
250236
}
251237

238+
/**
239+
* @param {Derived} current_value
240+
* @returns {boolean}
241+
*/
242+
function has_destroyed_component_ctx(current_value) {
243+
return current_value.ctx?.d ?? false;
244+
}
245+
252246
/**
253247
* This function is responsible for synchronizing a possibly bound prop with the inner component state.
254248
* It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value.
@@ -382,6 +376,11 @@ export function prop(props, key, flags, fallback) {
382376
return (inner_current_value.v = parent_value);
383377
});
384378

379+
// Ensure we eagerly capture the initial value if it's bindable
380+
if (bindable) {
381+
get(current_value);
382+
}
383+
385384
if (!immutable) current_value.equals = safe_equals;
386385

387386
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
@@ -408,11 +407,21 @@ export function prop(props, key, flags, fallback) {
408407
if (fallback_used && fallback_value !== undefined) {
409408
fallback_value = new_value;
410409
}
410+
411+
if (has_destroyed_component_ctx(current_value)) {
412+
return value;
413+
}
414+
411415
untrack(() => get(current_value)); // force a synchronisation immediately
412416
}
413417

414418
return value;
415419
}
420+
421+
if (has_destroyed_component_ctx(current_value)) {
422+
return current_value.v;
423+
}
424+
416425
return get(current_value);
417426
};
418427
}

‎packages/svelte/src/internal/client/reactivity/sources.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
derived_sources,
1515
set_derived_sources,
1616
check_dirtiness,
17-
untracking
17+
untracking,
18+
is_destroying_effect
1819
} from '../runtime.js';
1920
import { equals, safe_equals } from './equality.js';
2021
import {
@@ -34,6 +35,7 @@ import { get_stack } from '../dev/tracing.js';
3435
import { component_context, is_runes } from '../context.js';
3536

3637
export let inspect_effects = new Set();
38+
export const old_values = new Map();
3739

3840
/**
3941
* @param {Set<any>} v
@@ -168,6 +170,13 @@ export function set(source, value) {
168170
export function internal_set(source, value) {
169171
if (!source.equals(value)) {
170172
var old_value = source.v;
173+
174+
if (is_destroying_effect) {
175+
old_values.set(source, value);
176+
} else {
177+
old_values.set(source, old_value);
178+
}
179+
171180
source.v = value;
172181
source.wv = increment_write_version();
173182

‎packages/svelte/src/internal/client/runtime.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
BOUNDARY_EFFECT
2626
} from './constants.js';
2727
import { flush_tasks } from './dom/task.js';
28-
import { internal_set } from './reactivity/sources.js';
28+
import { internal_set, old_values } from './reactivity/sources.js';
2929
import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js';
3030
import * as e from './errors.js';
3131
import { FILENAME } from '../../constants.js';
@@ -673,6 +673,7 @@ function flush_queued_root_effects() {
673673
if (DEV) {
674674
dev_effect_stack = [];
675675
}
676+
old_values.clear();
676677
}
677678
}
678679

@@ -923,6 +924,10 @@ export function get(signal) {
923924
}
924925
}
925926

927+
if (is_destroying_effect && old_values.has(signal)) {
928+
return old_values.get(signal);
929+
}
930+
926931
return signal.v;
927932
}
928933

‎packages/svelte/src/internal/client/types.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type ComponentContext = {
1414
p: null | ComponentContext;
1515
/** context */
1616
c: null | Map<unknown, unknown>;
17+
/** destroyed */
18+
d: boolean;
1719
/** deferred effects */
1820
e: null | Array<{
1921
fn: () => void | (() => void);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
import { onDestroy } from 'svelte';
3+
4+
export let my_prop;
5+
6+
onDestroy(() => {
7+
console.log(my_prop.foo);
8+
});
9+
</script>
10+
11+
{my_prop.foo}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { test } from '../../test';
2+
import { flushSync } from 'svelte';
3+
4+
export default test({
5+
async test({ assert, target, logs }) {
6+
const [btn1] = target.querySelectorAll('button');
7+
8+
flushSync(() => {
9+
btn1.click();
10+
});
11+
12+
assert.deepEqual(logs, ['bar']);
13+
}
14+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
import Component from './Component.svelte';
3+
4+
let value = { foo: 'bar' };
5+
</script>
6+
7+
<button
8+
onclick={() => {
9+
value = undefined;
10+
}}>Reset value</button
11+
>
12+
13+
{#if value !== undefined}
14+
<Component my_prop={value} />
15+
{/if}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script lang="ts">
2+
export let ref;
3+
</script>
4+
5+
<input bind:this={ref} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { test } from '../../test';
2+
import { flushSync } from 'svelte';
3+
4+
export default test({
5+
async test({ target }) {
6+
const [btn1] = target.querySelectorAll('button');
7+
8+
btn1.click();
9+
flushSync();
10+
}
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
import Component from './Component.svelte';
3+
let state = { title: 'foo' };
4+
</script>
5+
6+
{#if state}
7+
{@const attributes = { title: state.title }}
8+
<Component {...attributes} />
9+
{/if}
10+
<button
11+
onclick={() => {
12+
state = undefined;
13+
}}
14+
>
15+
Del
16+
</button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
import { onDestroy } from "svelte";
3+
export let checked;
4+
export let count;
5+
onDestroy(() => {
6+
console.log(count, checked);
7+
});
8+
</script>
9+
10+
<p>{count}</p>
11+
12+
<button onclick={()=> count-- }></button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { test } from '../../test';
2+
import { flushSync } from 'svelte';
3+
4+
export default test({
5+
async test({ assert, target, logs }) {
6+
const [btn1, btn2, btn3] = target.querySelectorAll('button');
7+
let ps = [...target.querySelectorAll('p')];
8+
9+
for (const p of ps) {
10+
assert.equal(p.innerHTML, '0');
11+
}
12+
13+
flushSync(() => {
14+
btn1.click();
15+
});
16+
17+
// prop update normally if we are not unmounting
18+
for (const p of ps) {
19+
assert.equal(p.innerHTML, '1');
20+
}
21+
22+
flushSync(() => {
23+
btn3.click();
24+
});
25+
26+
// binding still works and update the value correctly
27+
for (const p of ps) {
28+
assert.equal(p.innerHTML, '0');
29+
}
30+
31+
flushSync(() => {
32+
btn1.click();
33+
});
34+
35+
flushSync(() => {
36+
btn1.click();
37+
});
38+
39+
console.warn(logs);
40+
41+
// the five components guarded by `count < 2` unmount and log
42+
assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]);
43+
44+
flushSync(() => {
45+
btn2.click();
46+
});
47+
48+
// the three components guarded by `show` unmount and log
49+
assert.deepEqual(logs, [
50+
1,
51+
true,
52+
1,
53+
true,
54+
1,
55+
true,
56+
1,
57+
true,
58+
1,
59+
true,
60+
2,
61+
true,
62+
2,
63+
true,
64+
2,
65+
true
66+
]);
67+
}
68+
});

0 commit comments

Comments
 (0)
Please sign in to comment.