Skip to content

Commit 1404623

Browse files
fix: re-evaluate derived props during teardown (#16278)
* Store deriveds old value before updating them for consistency with directly assigned sources when reading in teardown functions * Add tests for derived old values in teardown functions * Add tests for props old values in onDestroy hooks to prevent regressions * Add changeset * Update comment * remove extraneous tests - these pass with or without the src change * revert * WIP * simplify props * simplify * tweak * reorder a bit * simplify * unused * more * more * tweak * also appears to be unnecessary * changeset * fix * Update .changeset/light-rivers-jump.md * easier debugging * fix * WIP * fix --------- Co-authored-by: raythurnvoid <[email protected]>
1 parent eb094ba commit 1404623

File tree

9 files changed

+194
-25
lines changed

9 files changed

+194
-25
lines changed

.changeset/light-rivers-jump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: re-evaluate derived props during teardown

packages/svelte/src/internal/client/reactivity/deriveds.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { inspect_effects, set_inspect_effects } from './sources.js';
1919
import { get_stack } from '../dev/tracing.js';
2020
import { tracing_mode_flag } from '../../flags/index.js';
2121
import { component_context } from '../context.js';
22+
import { UNINITIALIZED } from '../../../constants.js';
2223

2324
/**
2425
* @template V
@@ -51,7 +52,7 @@ export function derived(fn) {
5152
fn,
5253
reactions: null,
5354
rv: 0,
54-
v: /** @type {V} */ (null),
55+
v: /** @type {V} */ (UNINITIALIZED),
5556
wv: 0,
5657
parent: parent_derived ?? active_effect,
5758
ac: null

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

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1-
/** @import { Derived, Source } from './types.js' */
1+
/** @import { ComponentContext } from '#client' */
2+
/** @import { Derived, Effect, Source } from './types.js' */
23
import { DEV } from 'esm-env';
34
import {
45
PROPS_IS_BINDABLE,
56
PROPS_IS_IMMUTABLE,
67
PROPS_IS_LAZY_INITIAL,
78
PROPS_IS_RUNES,
8-
PROPS_IS_UPDATED
9+
PROPS_IS_UPDATED,
10+
UNINITIALIZED
911
} from '../../../constants.js';
1012
import { get_descriptor, is_function } from '../../shared/utils.js';
1113
import { set, source, update } from './sources.js';
1214
import { derived, derived_safe_equal } from './deriveds.js';
13-
import { get, untrack } from '../runtime.js';
15+
import {
16+
active_effect,
17+
get,
18+
is_destroying_effect,
19+
set_active_effect,
20+
untrack
21+
} from '../runtime.js';
1422
import * as e from '../errors.js';
15-
import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
23+
import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
1624
import { proxy } from '../proxy.js';
1725
import { capture_store_binding } from './store.js';
1826
import { legacy_mode_flag } from '../../flags/index.js';
@@ -92,7 +100,7 @@ export function rest_props(props, exclude, name) {
92100

93101
/**
94102
* The proxy handler for legacy $$restProps and $$props
95-
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, special: Record<string | symbol, (v?: unknown) => unknown>, version: Source<number> }>}}
103+
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, special: Record<string | symbol, (v?: unknown) => unknown>, version: Source<number>, parent_effect: Effect }>}}
96104
*/
97105
const legacy_rest_props_handler = {
98106
get(target, key) {
@@ -102,17 +110,25 @@ const legacy_rest_props_handler = {
102110
},
103111
set(target, key, value) {
104112
if (!(key in target.special)) {
105-
// Handle props that can temporarily get out of sync with the parent
106-
/** @type {Record<string, (v?: unknown) => unknown>} */
107-
target.special[key] = prop(
108-
{
109-
get [key]() {
110-
return target.props[key];
111-
}
112-
},
113-
/** @type {string} */ (key),
114-
PROPS_IS_UPDATED
115-
);
113+
var previous_effect = active_effect;
114+
115+
try {
116+
set_active_effect(target.parent_effect);
117+
118+
// Handle props that can temporarily get out of sync with the parent
119+
/** @type {Record<string, (v?: unknown) => unknown>} */
120+
target.special[key] = prop(
121+
{
122+
get [key]() {
123+
return target.props[key];
124+
}
125+
},
126+
/** @type {string} */ (key),
127+
PROPS_IS_UPDATED
128+
);
129+
} finally {
130+
set_active_effect(previous_effect);
131+
}
116132
}
117133

118134
target.special[key](value);
@@ -151,7 +167,19 @@ const legacy_rest_props_handler = {
151167
* @returns {Record<string, unknown>}
152168
*/
153169
export function legacy_rest_props(props, exclude) {
154-
return new Proxy({ props, exclude, special: {}, version: source(0) }, legacy_rest_props_handler);
170+
return new Proxy(
171+
{
172+
props,
173+
exclude,
174+
special: {},
175+
version: source(0),
176+
// TODO this is only necessary because we need to track component
177+
// destruction inside `prop`, because of `bind:this`, but it
178+
// seems likely that we can simplify `bind:this` instead
179+
parent_effect: /** @type {Effect} */ (active_effect)
180+
},
181+
legacy_rest_props_handler
182+
);
155183
}
156184

157185
/**
@@ -366,16 +394,24 @@ export function prop(props, key, flags, fallback) {
366394
// create a derived that we can write to locally.
367395
// Or we are in legacy mode where we always create a derived to replicate that
368396
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
369-
var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter);
397+
var overridden = false;
398+
399+
var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(() => {
400+
overridden = false;
401+
return getter();
402+
});
370403

371404
// Capture the initial value if it's bindable
372405
if (bindable) get(d);
373406

407+
var parent_effect = /** @type {Effect} */ (active_effect);
408+
374409
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
375410
if (arguments.length > 0) {
376411
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
377412

378413
set(d, new_value);
414+
overridden = true;
379415

380416
if (fallback_value !== undefined) {
381417
fallback_value = new_value;
@@ -384,8 +420,12 @@ export function prop(props, key, flags, fallback) {
384420
return value;
385421
}
386422

387-
// TODO is this still necessary post-#16263?
388-
if (has_destroyed_component_ctx(d)) {
423+
// special case — avoid recalculating the derived if we're in a
424+
// teardown function and the prop was overridden locally, or the
425+
// component was already destroyed (this latter part is necessary
426+
// because `bind:this` can read props after the component has
427+
// been destroyed. TODO simplify `bind:this`
428+
if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
389429
return d.v;
390430
}
391431

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from './constants.js';
2828
import { flush_tasks } from './dom/task.js';
2929
import { internal_set, old_values } from './reactivity/sources.js';
30-
import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js';
30+
import { destroy_derived_effects, execute_derived, update_derived } from './reactivity/deriveds.js';
3131
import * as e from './errors.js';
3232

3333
import { tracing_mode_flag } from '../flags/index.js';
@@ -42,6 +42,7 @@ import {
4242
set_dev_stack
4343
} from './context.js';
4444
import { handle_error, invoke_error_boundary } from './error-handling.js';
45+
import { UNINITIALIZED } from '../../constants.js';
4546

4647
let is_flushing = false;
4748

@@ -795,7 +796,7 @@ export function get(signal) {
795796
}
796797
}
797798

798-
if (is_derived) {
799+
if (is_derived && !is_destroying_effect) {
799800
derived = /** @type {Derived} */ (signal);
800801

801802
if (check_dirtiness(derived)) {
@@ -836,13 +837,48 @@ export function get(signal) {
836837
}
837838
}
838839

839-
if (is_destroying_effect && old_values.has(signal)) {
840-
return old_values.get(signal);
840+
if (is_destroying_effect) {
841+
if (old_values.has(signal)) {
842+
return old_values.get(signal);
843+
}
844+
845+
if (is_derived) {
846+
derived = /** @type {Derived} */ (signal);
847+
848+
var value = derived.v;
849+
850+
// if the derived is dirty, or depends on the values that just changed, re-execute
851+
if ((derived.f & CLEAN) !== 0 || depends_on_old_values(derived)) {
852+
value = execute_derived(derived);
853+
}
854+
855+
old_values.set(derived, value);
856+
857+
return value;
858+
}
841859
}
842860

843861
return signal.v;
844862
}
845863

864+
/** @param {Derived} derived */
865+
function depends_on_old_values(derived) {
866+
if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst
867+
if (derived.deps === null) return false;
868+
869+
for (const dep of derived.deps) {
870+
if (old_values.has(dep)) {
871+
return true;
872+
}
873+
874+
if ((dep.f & DERIVED) !== 0 && depends_on_old_values(/** @type {Derived} */ (dep))) {
875+
return true;
876+
}
877+
}
878+
879+
return false;
880+
}
881+
846882
/**
847883
* Like `get`, but checks for `undefined`. Used for `var` declarations because they can be accessed before being declared
848884
* @template V
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, logs, target }) {
6+
const [increment] = target.querySelectorAll('button');
7+
8+
flushSync(() => increment.click());
9+
flushSync(() => increment.click());
10+
flushSync(() => increment.click());
11+
12+
assert.deepEqual(logs, [
13+
'count: 1',
14+
'squared: 1',
15+
'count: 2',
16+
'squared: 4',
17+
'count: 3',
18+
'squared: 9',
19+
'count: 4'
20+
]);
21+
}
22+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script>
2+
let count = $state(1);
3+
let squared = $derived(count * count);
4+
5+
$effect(() => {
6+
console.log(`count: ${count}`);
7+
8+
return () => {
9+
console.log(`squared: ${squared}`);
10+
};
11+
});
12+
</script>
13+
14+
<button onclick={() => count++}>increment</button>
15+
16+
<p>count: {count}</p>
17+
18+
{#if count % 2 === 0}
19+
<p id="squared">squared: {squared}</p>
20+
{/if}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
let { message, count } = $props();
3+
4+
$effect(() => () => {
5+
console.log(count, message);
6+
});
7+
</script>
8+
9+
<p>{count}</p>
10+
11+
<!-- we need these so that the props are made into deriveds -->
12+
<button disabled onclick={() => {
13+
count += 1;
14+
message += '!';
15+
}}>update</button>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { test } from '../../test';
2+
import { flushSync } from 'svelte';
3+
4+
export default test({
5+
async test({ assert, target, logs }) {
6+
const [increment, toggle] = target.querySelectorAll('button');
7+
8+
flushSync(() => toggle.click());
9+
assert.deepEqual(logs, [0, 'hello']);
10+
11+
flushSync(() => toggle.click());
12+
flushSync(() => increment.click());
13+
flushSync(() => increment.click());
14+
15+
assert.deepEqual(logs, [0, 'hello', 1, 'hello']);
16+
}
17+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import Component from "./Component.svelte";
3+
4+
let message = $state('hello');
5+
let count = $state(0);
6+
</script>
7+
8+
<button onclick={() => count++}>{count}</button>
9+
<button onclick={() => message = message === 'hello' ? 'goodbye' : 'hello'}>{message}</button>
10+
11+
{#if count < 2 && message === 'hello'}
12+
<Component {count} {message} />
13+
{/if}

0 commit comments

Comments
 (0)