From 42f5d10a42d8c6a0ac5f99f17f3805ea3048b2c0 Mon Sep 17 00:00:00 2001
From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com>
Date: Sun, 29 Jun 2025 15:49:15 +0100
Subject: [PATCH 01/25] Store deriveds old value before updating them for
consistency with directly assigned sources when reading in teardown functions
---
.../internal/client/reactivity/deriveds.js | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index e9cea0df3e64..ee2bf3ed2f81 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -15,7 +15,7 @@ import {
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import { destroy_effect } from './effects.js';
-import { inspect_effects, set_inspect_effects } from './sources.js';
+import { inspect_effects, old_values, set_inspect_effects } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { tracing_mode_flag } from '../../flags/index.js';
import { component_context } from '../context.js';
@@ -175,6 +175,23 @@ export function update_derived(derived) {
var value = execute_derived(derived);
if (!derived.equals(value)) {
+ // store old value before updating
+ // so that user_effect teardown functions
+ // can access the previous value.
+ // this is needed because derived updates happen early during
+ // effects dependency resolution (before cleanup),
+ // unlike direct state/derived updates, and this
+ // can happen also during template_effect execution
+ // that also happens before user_effect teardown.
+ //
+ // store old value only if not inside a teardown function
+ // because in legacy mode, bindable props are deriveds
+ // and they are executed during teardown.
+ if (!is_destroying_effect) {
+ var old_value = derived.v;
+ old_values.set(derived, old_value);
+ }
+
derived.v = value;
derived.wv = increment_write_version();
}
From 1fc9f0801c1ec583da9c69986925887377b8fffa Mon Sep 17 00:00:00 2001
From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com>
Date: Sun, 29 Jun 2025 16:45:35 +0100
Subject: [PATCH 02/25] Add tests for derived old values in teardown functions
---
.../derived-cleanup-old-value/_config.js | 39 +++++++++++++++++++
.../derived-cleanup-old-value/main.svelte | 17 ++++++++
2 files changed, 56 insertions(+)
create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js
new file mode 100644
index 000000000000..4810ba6fb985
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js
@@ -0,0 +1,39 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, logs, target }) {
+ /** @type {HTMLButtonElement | null} */
+ const increment_btn = target.querySelector('#increment');
+ /** @type {HTMLButtonElement | null} */
+ const overwrite_btn = target.querySelector('#overwrite');
+
+ // Initial state: count=1, derived_value=1
+
+ // Click to increment count: count=2, derived_value=4
+ flushSync(() => {
+ increment_btn?.click();
+ });
+
+ // Click to increment count: count=3, derived_value=9
+ flushSync(() => {
+ increment_btn?.click();
+ });
+
+ // Click to overwrite derived_value: count=3, derived_value=7
+ flushSync(() => {
+ overwrite_btn?.click();
+ });
+
+ // Should log old value during cleanup (4) and new value during setup (9)
+ assert.deepEqual(logs, [
+ '$effect: 1',
+ '$effect teardown: 1',
+ '$effect: 4',
+ '$effect teardown: 4',
+ '$effect: 9',
+ '$effect teardown: 9',
+ '$effect: 7'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte
new file mode 100644
index 000000000000..6cef12b1e345
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+
+
count: {count}
+derived_value: {derived_value}
From 9d9b64b94eb4a1b700d15da9fa08e793ed6ded06 Mon Sep 17 00:00:00 2001
From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com>
Date: Sun, 29 Jun 2025 16:46:58 +0100
Subject: [PATCH 03/25] Add tests for props old values in onDestroy hooks to
prevent regressions
---
.../Component.svelte | 11 ++++
.../_config.js | 63 +++++++++++++++++++
.../main.svelte | 52 +++++++++++++++
.../Component.svelte | 14 +++++
.../_config.js | 63 +++++++++++++++++++
.../main.svelte | 52 +++++++++++++++
.../Component.svelte | 11 ++++
.../_config.js | 49 +++++++++++++++
.../main.svelte | 41 ++++++++++++
9 files changed, 356 insertions(+)
create mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/Component.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/main.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/Component.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/main.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/Component.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/main.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/Component.svelte b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/Component.svelte
new file mode 100644
index 000000000000..40d6b1c2fbb4
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/Component.svelte
@@ -0,0 +1,11 @@
+
+
+Count: {count}
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/_config.js b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/_config.js
new file mode 100644
index 000000000000..07ea5af153a6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/_config.js
@@ -0,0 +1,63 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, logs, target }) {
+ /** @type {HTMLButtonElement | null} */
+ const increment_btn = target.querySelector('#increment');
+ /** @type {HTMLButtonElement | null} */
+ const toggle_btn = target.querySelector('#toggle');
+
+ for (const p of target.querySelectorAll('p')) {
+ assert.equal(p.innerHTML, 'Count: 0');
+ }
+
+ // Click increment: count=1
+ flushSync(() => {
+ increment_btn?.click();
+ });
+
+ for (const p of target.querySelectorAll('p')) {
+ assert.equal(p.innerHTML, 'Count: 1');
+ }
+
+ // Click increment again: count=2, components with count < 2 should unmount and log old values
+ flushSync(() => {
+ increment_btn?.click();
+ });
+
+ for (const p of target.querySelectorAll('p')) {
+ assert.equal(p.innerHTML, 'Count: 2');
+ }
+
+ // Toggle show to hide components that depend on show
+ flushSync(() => {
+ toggle_btn?.click();
+ });
+
+ // Should log old values during cleanup from the six components guarded by `count < 2`:
+ // 1. Component with bind: "1 true"
+ // 2. Component with spread: "1 true"
+ // 3. Component with normal props: "1 true"
+ // 4. Runes dynamic component with bind: "1 true"
+ // 5. Runes dynamic component with spread: "1 true"
+ // 6. Runes dynamic component with normal props: "1 true"
+ // Then from the four components guarded by `show`:
+ // 7. Component with bind (show): "2 true" (old value of checked)
+ // 8. Runes dynamic component with bind (show): "2 true"
+ // 9. Runes dynamic component with spread (show): "2 true"
+ // 10. Runes dynamic component with normal props (show): "2 true"
+ assert.deepEqual(logs, [
+ '1 true',
+ '1 true',
+ '1 true',
+ '1 true',
+ '1 true',
+ '1 true',
+ '2 true',
+ '2 true',
+ '2 true',
+ '2 true'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/main.svelte b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/main.svelte
new file mode 100644
index 000000000000..592c46ff429f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/main.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if show}
+
+{/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/Component.svelte b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/Component.svelte
new file mode 100644
index 000000000000..cc47017946ee
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/Component.svelte
@@ -0,0 +1,14 @@
+
+
+Count: {count}
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/_config.js b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/_config.js
new file mode 100644
index 000000000000..07ea5af153a6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/_config.js
@@ -0,0 +1,63 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, logs, target }) {
+ /** @type {HTMLButtonElement | null} */
+ const increment_btn = target.querySelector('#increment');
+ /** @type {HTMLButtonElement | null} */
+ const toggle_btn = target.querySelector('#toggle');
+
+ for (const p of target.querySelectorAll('p')) {
+ assert.equal(p.innerHTML, 'Count: 0');
+ }
+
+ // Click increment: count=1
+ flushSync(() => {
+ increment_btn?.click();
+ });
+
+ for (const p of target.querySelectorAll('p')) {
+ assert.equal(p.innerHTML, 'Count: 1');
+ }
+
+ // Click increment again: count=2, components with count < 2 should unmount and log old values
+ flushSync(() => {
+ increment_btn?.click();
+ });
+
+ for (const p of target.querySelectorAll('p')) {
+ assert.equal(p.innerHTML, 'Count: 2');
+ }
+
+ // Toggle show to hide components that depend on show
+ flushSync(() => {
+ toggle_btn?.click();
+ });
+
+ // Should log old values during cleanup from the six components guarded by `count < 2`:
+ // 1. Component with bind: "1 true"
+ // 2. Component with spread: "1 true"
+ // 3. Component with normal props: "1 true"
+ // 4. Runes dynamic component with bind: "1 true"
+ // 5. Runes dynamic component with spread: "1 true"
+ // 6. Runes dynamic component with normal props: "1 true"
+ // Then from the four components guarded by `show`:
+ // 7. Component with bind (show): "2 true" (old value of checked)
+ // 8. Runes dynamic component with bind (show): "2 true"
+ // 9. Runes dynamic component with spread (show): "2 true"
+ // 10. Runes dynamic component with normal props (show): "2 true"
+ assert.deepEqual(logs, [
+ '1 true',
+ '1 true',
+ '1 true',
+ '1 true',
+ '1 true',
+ '1 true',
+ '2 true',
+ '2 true',
+ '2 true',
+ '2 true'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/main.svelte b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/main.svelte
new file mode 100644
index 000000000000..592c46ff429f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/main.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if show}
+
+{/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/Component.svelte b/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/Component.svelte
new file mode 100644
index 000000000000..1a95f1edf55c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/Component.svelte
@@ -0,0 +1,11 @@
+
+
+Count: {count}
diff --git a/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/_config.js b/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/_config.js
new file mode 100644
index 000000000000..3d73bfd1a90c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/_config.js
@@ -0,0 +1,49 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, logs, target }) {
+ /** @type {HTMLButtonElement | null} */
+ const increment_btn = target.querySelector('#increment');
+ /** @type {HTMLButtonElement | null} */
+ const toggle_btn = target.querySelector('#toggle');
+
+ for (const p of target.querySelectorAll('p')) {
+ assert.equal(p.innerHTML, 'Count: 0');
+ }
+
+ // Click increment: count=1
+ flushSync(() => {
+ increment_btn?.click();
+ });
+
+ for (const p of target.querySelectorAll('p')) {
+ assert.equal(p.innerHTML, 'Count: 1');
+ }
+
+ // Click increment again: count=2, components with count < 2 should unmount and log old values
+ flushSync(() => {
+ increment_btn?.click();
+ });
+
+ for (const p of target.querySelectorAll('p')) {
+ assert.equal(p.innerHTML, 'Count: 2');
+ }
+
+ // Toggle show to hide components that depend on show
+ flushSync(() => {
+ toggle_btn?.click();
+ });
+
+ // Should log old values during cleanup from the four components guarded by `count < 2`:
+ // 1. Component with normal props: "1 true"
+ // 2. Component with spread: "1 true"
+ // 3. Runes dynamic component with normal props: "1 true"
+ // 4. Runes dynamic component with spread: "1 true"
+ // Then from the three components guarded by `show`:
+ // 5. Component with normal props (show): "2 true" (old value of checked)
+ // 6. Runes dynamic component with normal props (show): "2 true"
+ // 7. Runes dynamic component with spread (show): "2 true"
+ assert.deepEqual(logs, ['1 true', '1 true', '1 true', '1 true', '2 true', '2 true', '2 true']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/main.svelte
new file mode 100644
index 000000000000..8bc6abfa4d6b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/main.svelte
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if count < 2}
+
+{/if}
+
+
+{#if show}
+
+{/if}
+
+
+
+
+
+
+
+
+
+
+
+
From 5ed74b24bad2272f94169178db357f52d3e30d5d Mon Sep 17 00:00:00 2001
From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com>
Date: Sun, 29 Jun 2025 16:47:33 +0100
Subject: [PATCH 04/25] Add changeset
---
.changeset/light-rivers-jump.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/light-rivers-jump.md
diff --git a/.changeset/light-rivers-jump.md b/.changeset/light-rivers-jump.md
new file mode 100644
index 000000000000..9258401e8a86
--- /dev/null
+++ b/.changeset/light-rivers-jump.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+Store deriveds old value before updating them for consistency with directly assigned sources when reading in teardown functions
From b213de2caf344daa7d377fd1a5ee723565635804 Mon Sep 17 00:00:00 2001
From: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com>
Date: Sun, 29 Jun 2025 17:34:55 +0100
Subject: [PATCH 05/25] Update comment
---
.../svelte/src/internal/client/reactivity/deriveds.js | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index ee2bf3ed2f81..5e4e08cc6537 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -185,8 +185,12 @@ export function update_derived(derived) {
// that also happens before user_effect teardown.
//
// store old value only if not inside a teardown function
- // because in legacy mode, bindable props are deriveds
- // and they are executed during teardown.
+ // because we only need to save the old values before
+ // the cleanup is triggered othewise accessing
+ // a derived during cleanup will return the incorrect
+ // value in case the derived wasn't in the deps of the effect,
+ // or the teardown was executed because the component was
+ // destroyed.
if (!is_destroying_effect) {
var old_value = derived.v;
old_values.set(derived, old_value);
From 562c9e3c7beb1912b31cca8e513306b7a33a0f03 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 19:57:45 -0400
Subject: [PATCH 06/25] remove extraneous tests - these pass with or without
the src change
---
.../Component.svelte | 11 ----
.../_config.js | 63 -------------------
.../main.svelte | 52 ---------------
.../Component.svelte | 14 -----
.../_config.js | 63 -------------------
.../main.svelte | 52 ---------------
.../Component.svelte | 11 ----
.../_config.js | 49 ---------------
.../main.svelte | 41 ------------
9 files changed, 356 deletions(-)
delete mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/Component.svelte
delete mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/_config.js
delete mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/main.svelte
delete mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/Component.svelte
delete mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/_config.js
delete mode 100644 packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/main.svelte
delete mode 100644 packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/Component.svelte
delete mode 100644 packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/_config.js
delete mode 100644 packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/main.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/Component.svelte b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/Component.svelte
deleted file mode 100644
index 40d6b1c2fbb4..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/Component.svelte
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-Count: {count}
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/_config.js b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/_config.js
deleted file mode 100644
index 07ea5af153a6..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/_config.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-export default test({
- async test({ assert, logs, target }) {
- /** @type {HTMLButtonElement | null} */
- const increment_btn = target.querySelector('#increment');
- /** @type {HTMLButtonElement | null} */
- const toggle_btn = target.querySelector('#toggle');
-
- for (const p of target.querySelectorAll('p')) {
- assert.equal(p.innerHTML, 'Count: 0');
- }
-
- // Click increment: count=1
- flushSync(() => {
- increment_btn?.click();
- });
-
- for (const p of target.querySelectorAll('p')) {
- assert.equal(p.innerHTML, 'Count: 1');
- }
-
- // Click increment again: count=2, components with count < 2 should unmount and log old values
- flushSync(() => {
- increment_btn?.click();
- });
-
- for (const p of target.querySelectorAll('p')) {
- assert.equal(p.innerHTML, 'Count: 2');
- }
-
- // Toggle show to hide components that depend on show
- flushSync(() => {
- toggle_btn?.click();
- });
-
- // Should log old values during cleanup from the six components guarded by `count < 2`:
- // 1. Component with bind: "1 true"
- // 2. Component with spread: "1 true"
- // 3. Component with normal props: "1 true"
- // 4. Runes dynamic component with bind: "1 true"
- // 5. Runes dynamic component with spread: "1 true"
- // 6. Runes dynamic component with normal props: "1 true"
- // Then from the four components guarded by `show`:
- // 7. Component with bind (show): "2 true" (old value of checked)
- // 8. Runes dynamic component with bind (show): "2 true"
- // 9. Runes dynamic component with spread (show): "2 true"
- // 10. Runes dynamic component with normal props (show): "2 true"
- assert.deepEqual(logs, [
- '1 true',
- '1 true',
- '1 true',
- '1 true',
- '1 true',
- '1 true',
- '2 true',
- '2 true',
- '2 true',
- '2 true'
- ]);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/main.svelte b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/main.svelte
deleted file mode 100644
index 592c46ff429f..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-no-setter/main.svelte
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-{#if count < 2}
-
-{/if}
-
-
-{#if count < 2}
-
-{/if}
-
-
-{#if count < 2}
-
-{/if}
-
-
-{#if show}
-
-{/if}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/Component.svelte b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/Component.svelte
deleted file mode 100644
index cc47017946ee..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/Component.svelte
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-Count: {count}
-
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/_config.js b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/_config.js
deleted file mode 100644
index 07ea5af153a6..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/_config.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-export default test({
- async test({ assert, logs, target }) {
- /** @type {HTMLButtonElement | null} */
- const increment_btn = target.querySelector('#increment');
- /** @type {HTMLButtonElement | null} */
- const toggle_btn = target.querySelector('#toggle');
-
- for (const p of target.querySelectorAll('p')) {
- assert.equal(p.innerHTML, 'Count: 0');
- }
-
- // Click increment: count=1
- flushSync(() => {
- increment_btn?.click();
- });
-
- for (const p of target.querySelectorAll('p')) {
- assert.equal(p.innerHTML, 'Count: 1');
- }
-
- // Click increment again: count=2, components with count < 2 should unmount and log old values
- flushSync(() => {
- increment_btn?.click();
- });
-
- for (const p of target.querySelectorAll('p')) {
- assert.equal(p.innerHTML, 'Count: 2');
- }
-
- // Toggle show to hide components that depend on show
- flushSync(() => {
- toggle_btn?.click();
- });
-
- // Should log old values during cleanup from the six components guarded by `count < 2`:
- // 1. Component with bind: "1 true"
- // 2. Component with spread: "1 true"
- // 3. Component with normal props: "1 true"
- // 4. Runes dynamic component with bind: "1 true"
- // 5. Runes dynamic component with spread: "1 true"
- // 6. Runes dynamic component with normal props: "1 true"
- // Then from the four components guarded by `show`:
- // 7. Component with bind (show): "2 true" (old value of checked)
- // 8. Runes dynamic component with bind (show): "2 true"
- // 9. Runes dynamic component with spread (show): "2 true"
- // 10. Runes dynamic component with normal props (show): "2 true"
- assert.deepEqual(logs, [
- '1 true',
- '1 true',
- '1 true',
- '1 true',
- '1 true',
- '1 true',
- '2 true',
- '2 true',
- '2 true',
- '2 true'
- ]);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/main.svelte b/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/main.svelte
deleted file mode 100644
index 592c46ff429f..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/bindable-props-cleanup-with-setter/main.svelte
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-{#if count < 2}
-
-{/if}
-
-
-{#if count < 2}
-
-{/if}
-
-
-{#if count < 2}
-
-{/if}
-
-
-{#if show}
-
-{/if}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/Component.svelte b/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/Component.svelte
deleted file mode 100644
index 1a95f1edf55c..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/Component.svelte
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-Count: {count}
diff --git a/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/_config.js b/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/_config.js
deleted file mode 100644
index 3d73bfd1a90c..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/_config.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { flushSync } from 'svelte';
-import { test } from '../../test';
-
-export default test({
- async test({ assert, logs, target }) {
- /** @type {HTMLButtonElement | null} */
- const increment_btn = target.querySelector('#increment');
- /** @type {HTMLButtonElement | null} */
- const toggle_btn = target.querySelector('#toggle');
-
- for (const p of target.querySelectorAll('p')) {
- assert.equal(p.innerHTML, 'Count: 0');
- }
-
- // Click increment: count=1
- flushSync(() => {
- increment_btn?.click();
- });
-
- for (const p of target.querySelectorAll('p')) {
- assert.equal(p.innerHTML, 'Count: 1');
- }
-
- // Click increment again: count=2, components with count < 2 should unmount and log old values
- flushSync(() => {
- increment_btn?.click();
- });
-
- for (const p of target.querySelectorAll('p')) {
- assert.equal(p.innerHTML, 'Count: 2');
- }
-
- // Toggle show to hide components that depend on show
- flushSync(() => {
- toggle_btn?.click();
- });
-
- // Should log old values during cleanup from the four components guarded by `count < 2`:
- // 1. Component with normal props: "1 true"
- // 2. Component with spread: "1 true"
- // 3. Runes dynamic component with normal props: "1 true"
- // 4. Runes dynamic component with spread: "1 true"
- // Then from the three components guarded by `show`:
- // 5. Component with normal props (show): "2 true" (old value of checked)
- // 6. Runes dynamic component with normal props (show): "2 true"
- // 7. Runes dynamic component with spread (show): "2 true"
- assert.deepEqual(logs, ['1 true', '1 true', '1 true', '1 true', '2 true', '2 true', '2 true']);
- }
-});
diff --git a/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/main.svelte
deleted file mode 100644
index 8bc6abfa4d6b..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/regular-props-cleanup-old-value/main.svelte
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-{#if count < 2}
-
-{/if}
-
-
-{#if count < 2}
-
-{/if}
-
-
-{#if show}
-
-{/if}
-
-
-
-
-
-
-
-
-
-
-
-
From 85268bc147341dce4ea60cb2cb08be403efad30c Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 19:59:46 -0400
Subject: [PATCH 07/25] revert
---
.../internal/client/reactivity/deriveds.js | 23 +------------------
1 file changed, 1 insertion(+), 22 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index 5e4e08cc6537..e9cea0df3e64 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -15,7 +15,7 @@ import {
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
import { destroy_effect } from './effects.js';
-import { inspect_effects, old_values, set_inspect_effects } from './sources.js';
+import { inspect_effects, set_inspect_effects } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { tracing_mode_flag } from '../../flags/index.js';
import { component_context } from '../context.js';
@@ -175,27 +175,6 @@ export function update_derived(derived) {
var value = execute_derived(derived);
if (!derived.equals(value)) {
- // store old value before updating
- // so that user_effect teardown functions
- // can access the previous value.
- // this is needed because derived updates happen early during
- // effects dependency resolution (before cleanup),
- // unlike direct state/derived updates, and this
- // can happen also during template_effect execution
- // that also happens before user_effect teardown.
- //
- // store old value only if not inside a teardown function
- // because we only need to save the old values before
- // the cleanup is triggered othewise accessing
- // a derived during cleanup will return the incorrect
- // value in case the derived wasn't in the deps of the effect,
- // or the teardown was executed because the component was
- // destroyed.
- if (!is_destroying_effect) {
- var old_value = derived.v;
- old_values.set(derived, old_value);
- }
-
derived.v = value;
derived.wv = increment_write_version();
}
From 0ad95885f397953b263b7906660ce16535cfc263 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 20:41:25 -0400
Subject: [PATCH 08/25] WIP
---
.../svelte/src/internal/client/runtime.js | 19 +++++++--
.../derived-cleanup-old-value/_config.js | 39 ++++++-------------
.../derived-cleanup-old-value/main.svelte | 15 ++++---
3 files changed, 35 insertions(+), 38 deletions(-)
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index d057bfdf0d9d..a8dc6904d82d 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -26,7 +26,7 @@ import {
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js';
-import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js';
+import { destroy_derived_effects, execute_derived, update_derived } from './reactivity/deriveds.js';
import * as e from './errors.js';
import { tracing_mode_flag } from '../flags/index.js';
@@ -764,7 +764,7 @@ export function get(signal) {
}
}
- if (is_derived) {
+ if (is_derived && !is_destroying_effect) {
derived = /** @type {Derived} */ (signal);
if (check_dirtiness(derived)) {
@@ -805,8 +805,19 @@ export function get(signal) {
}
}
- if (is_destroying_effect && old_values.has(signal)) {
- return old_values.get(signal);
+ if (is_destroying_effect) {
+ if (old_values.has(signal)) {
+ return old_values.get(signal);
+ }
+
+ if (is_derived) {
+ derived = /** @type {Derived} */ (signal);
+
+ var value = execute_derived(derived);
+ old_values.set(derived, value);
+
+ return value;
+ }
}
return signal.v;
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js
index 4810ba6fb985..694dccdcf8a0 100644
--- a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js
@@ -3,37 +3,20 @@ import { test } from '../../test';
export default test({
async test({ assert, logs, target }) {
- /** @type {HTMLButtonElement | null} */
- const increment_btn = target.querySelector('#increment');
- /** @type {HTMLButtonElement | null} */
- const overwrite_btn = target.querySelector('#overwrite');
+ const [increment] = target.querySelectorAll('button');
- // Initial state: count=1, derived_value=1
+ flushSync(() => increment.click());
+ flushSync(() => increment.click());
+ flushSync(() => increment.click());
- // Click to increment count: count=2, derived_value=4
- flushSync(() => {
- increment_btn?.click();
- });
-
- // Click to increment count: count=3, derived_value=9
- flushSync(() => {
- increment_btn?.click();
- });
-
- // Click to overwrite derived_value: count=3, derived_value=7
- flushSync(() => {
- overwrite_btn?.click();
- });
-
- // Should log old value during cleanup (4) and new value during setup (9)
assert.deepEqual(logs, [
- '$effect: 1',
- '$effect teardown: 1',
- '$effect: 4',
- '$effect teardown: 4',
- '$effect: 9',
- '$effect teardown: 9',
- '$effect: 7'
+ 'count: 1',
+ 'squared: 1',
+ 'count: 2',
+ 'squared: 4',
+ 'count: 3',
+ 'squared: 9',
+ 'count: 4'
]);
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte
index 6cef12b1e345..a4c58a8e991c 100644
--- a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte
@@ -1,17 +1,20 @@
-
-
+
count: {count}
-derived_value: {derived_value}
+
+{#if count % 2 === 0}
+ squared: {squared}
+{/if}
From 8a6d5ed34b3532062592bfb303e21aae3356dfcc Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 21:23:25 -0400
Subject: [PATCH 09/25] simplify props
---
.../src/internal/client/reactivity/props.js | 28 ++-----------------
1 file changed, 3 insertions(+), 25 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index f3111361c051..03f4938fbe2d 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -362,52 +362,30 @@ export function prop(props, key, flags, fallback) {
// hard mode. this is where it gets ugly — the value in the child should
// synchronize with the parent, but it should also be possible to temporarily
// set the value to something else locally.
- var from_child = false;
- var was_from_child = false;
// The derived returns the current value. The underlying mutable
// source is written to from various places to persist this value.
- var inner_current_value = mutable_source(prop_value);
- var current_value = derived(() => {
- var parent_value = getter();
- var child_value = get(inner_current_value);
-
- if (from_child) {
- from_child = false;
- was_from_child = true;
- return child_value;
- }
-
- was_from_child = false;
- return (inner_current_value.v = parent_value);
- });
+ var current_value = (immutable ? derived : derived_safe_equal)(getter);
// Ensure we eagerly capture the initial value if it's bindable
if (bindable) {
get(current_value);
}
- if (!immutable) current_value.equals = safe_equals;
-
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
// legacy nonsense — need to ensure the source is invalidated when necessary
// also needed for when handling inspect logic so we can inspect the correct source signal
if (captured_signals !== null) {
- // set this so that we don't reset to the parent value if `d`
- // is invalidated because of `invalidate_inner_signals` (rather
- // than because the parent or child value changed)
- from_child = was_from_child;
// invoke getters so that signals are picked up by `invalidate_inner_signals`
getter();
- get(inner_current_value);
+ get(current_value);
}
if (arguments.length > 0) {
const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value;
if (!current_value.equals(new_value)) {
- from_child = true;
- set(inner_current_value, new_value);
+ set(current_value, new_value);
// To ensure the fallback value is consistent when used with proxies, we
// update the local fallback_value, but only if the fallback is actively used
if (fallback_used && fallback_value !== undefined) {
From e0ebcc8b5d4368917d34b9062d1a30d62282f820 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 21:33:04 -0400
Subject: [PATCH 10/25] simplify
---
.../svelte/src/internal/client/reactivity/props.js | 14 ++------------
1 file changed, 2 insertions(+), 12 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 03f4938fbe2d..1d27c2fb0f1a 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -373,35 +373,25 @@ export function prop(props, key, flags, fallback) {
}
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
- // legacy nonsense — need to ensure the source is invalidated when necessary
- // also needed for when handling inspect logic so we can inspect the correct source signal
- if (captured_signals !== null) {
- // invoke getters so that signals are picked up by `invalidate_inner_signals`
- getter();
- get(current_value);
- }
-
if (arguments.length > 0) {
const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value;
if (!current_value.equals(new_value)) {
set(current_value, new_value);
+
// To ensure the fallback value is consistent when used with proxies, we
// update the local fallback_value, but only if the fallback is actively used
if (fallback_used && fallback_value !== undefined) {
fallback_value = new_value;
}
- if (has_destroyed_component_ctx(current_value)) {
- return value;
- }
-
untrack(() => get(current_value)); // force a synchronisation immediately
}
return value;
}
+ // TODO is this still necessary post-#16263?
if (has_destroyed_component_ctx(current_value)) {
return current_value.v;
}
From 9506889f80358f9c52dc8e4ae3b8981b0f3b5534 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 21:41:54 -0400
Subject: [PATCH 11/25] tweak
---
.../src/internal/client/reactivity/props.js | 20 +++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 1d27c2fb0f1a..37e53b5a6dc1 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -277,11 +277,17 @@ export function prop(props, key, flags, fallback) {
// or `createClassComponent(Component, props)`
var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
- var setter =
- (bindable &&
- (get_descriptor(props, key)?.set ??
- (is_entry_props && key in props && ((v) => (props[key] = v))))) ||
- undefined;
+ /** @type {((v: V) => void) | undefined} */
+ var setter;
+
+ /** @type {() => V} */
+ var getter;
+
+ if (bindable) {
+ setter =
+ get_descriptor(props, key)?.set ??
+ (is_entry_props && key in props ? (v) => (props[key] = v) : undefined);
+ }
var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true;
@@ -307,11 +313,9 @@ export function prop(props, key, flags, fallback) {
}
prop_value = get_fallback();
- if (setter) setter(prop_value);
+ setter?.(prop_value);
}
- /** @type {() => V} */
- var getter;
if (runes) {
getter = () => {
var value = /** @type {V} */ (props[key]);
From 2a16f17de717e6ad6859d2e35d8786f43762adb7 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 21:48:35 -0400
Subject: [PATCH 12/25] reorder a bit
---
.../src/internal/client/reactivity/props.js | 63 ++++++++++---------
1 file changed, 32 insertions(+), 31 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 37e53b5a6dc1..101e8f2dd242 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -264,18 +264,24 @@ export function prop(props, key, flags, fallback) {
var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0;
var bindable = (flags & PROPS_IS_BINDABLE) !== 0;
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
- var is_store_sub = false;
- var prop_value;
- if (bindable) {
- [prop_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
- } else {
- prop_value = /** @type {V} */ (props[key]);
- }
+ var fallback_value = /** @type {V} */ (fallback);
+ var fallback_dirty = true;
+ var fallback_used = false;
+
+ var get_fallback = () => {
+ fallback_used = true;
- // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
- // or `createClassComponent(Component, props)`
- var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
+ if (fallback_dirty) {
+ fallback_dirty = false;
+
+ fallback_value = lazy
+ ? untrack(/** @type {() => V} */ (fallback))
+ : /** @type {V} */ (fallback);
+ }
+
+ return fallback_value;
+ };
/** @type {((v: V) => void) | undefined} */
var setter;
@@ -284,36 +290,31 @@ export function prop(props, key, flags, fallback) {
var getter;
if (bindable) {
+ // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
+ // or `createClassComponent(Component, props)`
+ var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
+
setter =
get_descriptor(props, key)?.set ??
(is_entry_props && key in props ? (v) => (props[key] = v) : undefined);
}
- var fallback_value = /** @type {V} */ (fallback);
- var fallback_dirty = true;
- var fallback_used = false;
+ var initial_value;
+ var is_store_sub = false;
- var get_fallback = () => {
- fallback_used = true;
- if (fallback_dirty) {
- fallback_dirty = false;
- if (lazy) {
- fallback_value = untrack(/** @type {() => V} */ (fallback));
- } else {
- fallback_value = /** @type {V} */ (fallback);
- }
- }
+ if (bindable) {
+ [initial_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
+ } else {
+ initial_value = /** @type {V} */ (props[key]);
+ }
- return fallback_value;
- };
+ if (initial_value === undefined && fallback !== undefined) {
+ initial_value = get_fallback();
- if (prop_value === undefined && fallback !== undefined) {
- if (setter && runes) {
- e.props_invalid_value(key);
+ if (setter) {
+ if (runes) e.props_invalid_value(key);
+ setter(initial_value);
}
-
- prop_value = get_fallback();
- setter?.(prop_value);
}
if (runes) {
From b141f60a7f45770939011605115ec41516dfdb58 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 21:53:24 -0400
Subject: [PATCH 13/25] simplify
---
packages/svelte/src/internal/client/constants.js | 2 --
.../src/internal/client/reactivity/props.js | 15 ++++-----------
packages/svelte/src/internal/client/runtime.js | 13 +------------
3 files changed, 5 insertions(+), 25 deletions(-)
diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js
index dd3d1b2df636..cd5e0d224444 100644
--- a/packages/svelte/src/internal/client/constants.js
+++ b/packages/svelte/src/internal/client/constants.js
@@ -15,8 +15,6 @@ export const DESTROYED = 1 << 14;
export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 16;
-/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
-export const LEGACY_DERIVED_PROP = 1 << 17;
export const INSPECT_EFFECT = 1 << 18;
export const HEAD_EFFECT = 1 << 19;
export const EFFECT_HAS_DERIVED = 1 << 20;
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 101e8f2dd242..42a905f6682f 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -8,12 +8,11 @@ import {
PROPS_IS_UPDATED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js';
-import { mutable_source, set, source, update } from './sources.js';
+import { set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
-import { get, captured_signals, untrack } from '../runtime.js';
-import { safe_equals } from './equality.js';
+import { get, untrack } from '../runtime.js';
import * as e from '../errors.js';
-import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
+import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
@@ -326,14 +325,8 @@ export function prop(props, key, flags, fallback) {
return value;
};
} else {
- // Svelte 4 did not trigger updates when a primitive value was updated to the same value.
- // Replicate that behavior through using a derived
- var derived_getter = (immutable ? derived : derived_safe_equal)(
- () => /** @type {V} */ (props[key])
- );
- derived_getter.f |= LEGACY_DERIVED_PROP;
getter = () => {
- var value = get(derived_getter);
+ var value = /** @type {V} */ (props[key]);
if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
return value === undefined ? fallback_value : value;
};
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 3e70537d7cbb..5a798ba3e996 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -20,7 +20,6 @@ import {
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT,
- LEGACY_DERIVED_PROP,
DISCONNECTED,
EFFECT_IS_UPDATING,
STALE_REACTION
@@ -863,17 +862,7 @@ export function invalidate_inner_signals(fn) {
var captured = capture_signals(() => untrack(fn));
for (var signal of captured) {
- // Go one level up because derived signals created as part of props in legacy mode
- if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
- for (const dep of /** @type {Derived} */ (signal).deps || []) {
- if ((dep.f & DERIVED) === 0) {
- // Use internal_set instead of set here and below to avoid mutation validation
- internal_set(dep, dep.v);
- }
- }
- } else {
- internal_set(signal, signal.v);
- }
+ internal_set(signal, signal.v);
}
}
From ab1ea276259c83335bc30a4bc94e30393a21ca26 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 21:54:31 -0400
Subject: [PATCH 14/25] unused
---
packages/svelte/src/internal/client/reactivity/props.js | 2 --
1 file changed, 2 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 42a905f6682f..919ad37f35fa 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -382,8 +382,6 @@ export function prop(props, key, flags, fallback) {
if (fallback_used && fallback_value !== undefined) {
fallback_value = new_value;
}
-
- untrack(() => get(current_value)); // force a synchronisation immediately
}
return value;
From bdd5897e1346992ff47c4e19596e79da1afa2651 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 21:58:58 -0400
Subject: [PATCH 15/25] more
---
.../svelte/src/internal/client/reactivity/props.js | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 919ad37f35fa..f09883f5b4e7 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -374,14 +374,12 @@ export function prop(props, key, flags, fallback) {
if (arguments.length > 0) {
const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value;
- if (!current_value.equals(new_value)) {
- set(current_value, new_value);
+ set(current_value, new_value);
- // To ensure the fallback value is consistent when used with proxies, we
- // update the local fallback_value, but only if the fallback is actively used
- if (fallback_used && fallback_value !== undefined) {
- fallback_value = new_value;
- }
+ // To ensure the fallback value is consistent when used with proxies, we
+ // update the local fallback_value, but only if the fallback is actively used
+ if (fallback_used && fallback_value !== undefined) {
+ fallback_value = new_value;
}
return value;
From 125388655952f7f0d7bce3793c4a5ad9b57bfaa0 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 22:08:23 -0400
Subject: [PATCH 16/25] more
---
.../src/internal/client/reactivity/props.js | 46 +++++++++----------
1 file changed, 21 insertions(+), 25 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index f09883f5b4e7..9420b8e94fef 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -285,9 +285,6 @@ export function prop(props, key, flags, fallback) {
/** @type {((v: V) => void) | undefined} */
var setter;
- /** @type {() => V} */
- var getter;
-
if (bindable) {
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
@@ -316,6 +313,9 @@ export function prop(props, key, flags, fallback) {
}
}
+ /** @type {() => V} */
+ var getter;
+
if (runes) {
getter = () => {
var value = /** @type {V} */ (props[key]);
@@ -332,15 +332,16 @@ export function prop(props, key, flags, fallback) {
};
}
- // easy mode — prop is never written to
- if ((flags & PROPS_IS_UPDATED) === 0 && runes) {
+ // prop is never written to — we only need a getter
+ if (runes && (flags & PROPS_IS_UPDATED) === 0) {
return getter;
}
- // intermediate mode — prop is written to, but the parent component had
- // `bind:foo` which means we can just call `$$props.foo = value` directly
+ // prop is written to, but the parent component had `bind:foo` which
+ // means we can just call `$$props.foo = value` directly
if (setter) {
var legacy_parent = props.$$legacy;
+
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode.
@@ -350,31 +351,26 @@ export function prop(props, key, flags, fallback) {
if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value);
}
+
return value;
- } else {
- return getter();
}
+
+ return getter();
};
}
- // hard mode. this is where it gets ugly — the value in the child should
- // synchronize with the parent, but it should also be possible to temporarily
- // set the value to something else locally.
+ // prop is written to, but there's no binding, which means we
+ // create a derived that we can write to locally
+ var d = (immutable ? derived : derived_safe_equal)(getter);
- // The derived returns the current value. The underlying mutable
- // source is written to from various places to persist this value.
- var current_value = (immutable ? derived : derived_safe_equal)(getter);
-
- // Ensure we eagerly capture the initial value if it's bindable
- if (bindable) {
- get(current_value);
- }
+ // Capture the initial value if it's bindable
+ if (bindable) get(d);
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
- const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value;
+ const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
- set(current_value, new_value);
+ set(d, new_value);
// To ensure the fallback value is consistent when used with proxies, we
// update the local fallback_value, but only if the fallback is actively used
@@ -386,10 +382,10 @@ export function prop(props, key, flags, fallback) {
}
// TODO is this still necessary post-#16263?
- if (has_destroyed_component_ctx(current_value)) {
- return current_value.v;
+ if (has_destroyed_component_ctx(d)) {
+ return d.v;
}
- return get(current_value);
+ return get(d);
};
}
From c9cf7e3044a01545acb603e2c42d00441a67ccd2 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 22:10:12 -0400
Subject: [PATCH 17/25] tweak
---
packages/svelte/src/internal/client/reactivity/props.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 9420b8e94fef..de64bd7806e1 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -259,7 +259,6 @@ function has_destroyed_component_ctx(current_value) {
* @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))}
*/
export function prop(props, key, flags, fallback) {
- var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0;
var bindable = (flags & PROPS_IS_BINDABLE) !== 0;
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
@@ -361,7 +360,7 @@ export function prop(props, key, flags, fallback) {
// prop is written to, but there's no binding, which means we
// create a derived that we can write to locally
- var d = (immutable ? derived : derived_safe_equal)(getter);
+ var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter);
// Capture the initial value if it's bindable
if (bindable) get(d);
From 73d66a8dbf83d45a617251e1fa599ace329b8ce2 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 22:14:20 -0400
Subject: [PATCH 18/25] also appears to be unnecessary
---
packages/svelte/src/internal/client/reactivity/props.js | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index de64bd7806e1..a7f9daf34c2c 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -265,11 +265,8 @@ export function prop(props, key, flags, fallback) {
var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true;
- var fallback_used = false;
var get_fallback = () => {
- fallback_used = true;
-
if (fallback_dirty) {
fallback_dirty = false;
@@ -320,7 +317,6 @@ export function prop(props, key, flags, fallback) {
var value = /** @type {V} */ (props[key]);
if (value === undefined) return get_fallback();
fallback_dirty = true;
- fallback_used = false;
return value;
};
} else {
@@ -371,9 +367,7 @@ export function prop(props, key, flags, fallback) {
set(d, new_value);
- // To ensure the fallback value is consistent when used with proxies, we
- // update the local fallback_value, but only if the fallback is actively used
- if (fallback_used && fallback_value !== undefined) {
+ if (fallback_value !== undefined) {
fallback_value = new_value;
}
From dfbce4dd61aa86c556977a2e27c25a310d9b9fca Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Mon, 30 Jun 2025 22:16:12 -0400
Subject: [PATCH 19/25] changeset
---
.changeset/new-trees-behave.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/new-trees-behave.md
diff --git a/.changeset/new-trees-behave.md b/.changeset/new-trees-behave.md
new file mode 100644
index 000000000000..d5fab30f3ea2
--- /dev/null
+++ b/.changeset/new-trees-behave.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+chore: simplify props
From 884d4319d866bed78d52bf8694fd534f7ab26958 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 1 Jul 2025 07:50:11 -0400
Subject: [PATCH 20/25] fix
---
packages/svelte/src/internal/client/runtime.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 4fa40bd7831b..c217ac272285 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -818,7 +818,7 @@ export function get(signal) {
if (is_derived) {
derived = /** @type {Derived} */ (signal);
- var value = execute_derived(derived);
+ var value = (derived.f & CLEAN) !== 0 ? execute_derived(derived) : derived.v;
old_values.set(derived, value);
return value;
From 809693c62a9a351d55ea14ad2d26637c69418cb8 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 1 Jul 2025 14:35:12 -0400
Subject: [PATCH 21/25] Update .changeset/light-rivers-jump.md
---
.changeset/light-rivers-jump.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.changeset/light-rivers-jump.md b/.changeset/light-rivers-jump.md
index 9258401e8a86..2454d5715602 100644
--- a/.changeset/light-rivers-jump.md
+++ b/.changeset/light-rivers-jump.md
@@ -2,4 +2,4 @@
'svelte': patch
---
-Store deriveds old value before updating them for consistency with directly assigned sources when reading in teardown functions
+fix: re-evaluate derived props during teardown
From f32818cfedef4f9b1744175027941e6aac46026b Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 1 Jul 2025 15:25:00 -0400
Subject: [PATCH 22/25] easier debugging
---
packages/svelte/src/internal/client/reactivity/deriveds.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index d3123d24a1ca..37e8643e1618 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -19,6 +19,7 @@ import { inspect_effects, set_inspect_effects } from './sources.js';
import { get_stack } from '../dev/tracing.js';
import { tracing_mode_flag } from '../../flags/index.js';
import { component_context } from '../context.js';
+import { UNINITIALIZED } from '../../../constants.js';
/**
* @template V
@@ -51,7 +52,7 @@ export function derived(fn) {
fn,
reactions: null,
rv: 0,
- v: /** @type {V} */ (null),
+ v: /** @type {V} */ (UNINITIALIZED),
wv: 0,
parent: parent_derived ?? active_effect,
ac: null
From 4a898981e132af6f14e414853215cfa75e0664bd Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 1 Jul 2025 16:07:37 -0400
Subject: [PATCH 23/25] fix
---
.../src/internal/client/reactivity/props.js | 4 ++-
.../svelte/src/internal/client/runtime.js | 27 ++++++++++++++++++-
2 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index bd0cc314cf49..ee07a342a8df 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -1,3 +1,4 @@
+/** @import { ComponentContext } from '#client' */
/** @import { Derived, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
@@ -5,7 +6,8 @@ import {
PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES,
- PROPS_IS_UPDATED
+ PROPS_IS_UPDATED,
+ UNINITIALIZED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js';
import { set, source, update } from './sources.js';
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index c217ac272285..b9a7d9c8a436 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -40,6 +40,7 @@ import {
} from './context.js';
import { handle_error, invoke_error_boundary } from './error-handling.js';
import { snapshot } from '../shared/clone.js';
+import { UNINITIALIZED } from '../../constants.js';
let is_flushing = false;
@@ -818,7 +819,13 @@ export function get(signal) {
if (is_derived) {
derived = /** @type {Derived} */ (signal);
- var value = (derived.f & CLEAN) !== 0 ? execute_derived(derived) : derived.v;
+ var value = derived.v;
+
+ // if the derived is dirty, or depends on the values that just changed, re-execute
+ if ((derived.f & CLEAN) !== 0 || depends_on_old_values(derived)) {
+ value = execute_derived(derived);
+ }
+
old_values.set(derived, value);
return value;
@@ -828,6 +835,24 @@ export function get(signal) {
return signal.v;
}
+/** @param {Derived} derived */
+function depends_on_old_values(derived) {
+ if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst
+ if (derived.deps === null) return false;
+
+ for (const dep of derived.deps) {
+ if (old_values.has(dep)) {
+ return true;
+ }
+
+ if ((dep.f & DERIVED) !== 0 && depends_on_old_values(/** @type {Derived} */ (dep))) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
/**
* Like `get`, but checks for `undefined`. Used for `var` declarations because they can be accessed before being declared
* @template V
From 05636d309b7bc891affd8c4011982ed9d8167e63 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 1 Jul 2025 22:15:40 -0400
Subject: [PATCH 24/25] WIP
---
.../src/internal/client/reactivity/props.js | 15 ++++++++++++---
.../effect-teardown-derived/Component.svelte | 15 +++++++++++++++
.../samples/effect-teardown-derived/_config.js | 17 +++++++++++++++++
.../samples/effect-teardown-derived/main.svelte | 13 +++++++++++++
4 files changed, 57 insertions(+), 3 deletions(-)
create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte
create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index ee07a342a8df..33f0e67a6d1d 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -12,7 +12,7 @@ import {
import { get_descriptor, is_function } from '../../shared/utils.js';
import { set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
-import { get, untrack } from '../runtime.js';
+import { get, is_destroying_effect, untrack } from '../runtime.js';
import * as e from '../errors.js';
import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
@@ -366,7 +366,12 @@ export function prop(props, key, flags, fallback) {
// prop is written to, but there's no binding, which means we
// create a derived that we can write to locally
- var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter);
+ var overridden = false;
+
+ var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(() => {
+ overridden = false;
+ return getter();
+ });
// Capture the initial value if it's bindable
if (bindable) get(d);
@@ -376,6 +381,7 @@ export function prop(props, key, flags, fallback) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
set(d, new_value);
+ overridden = true;
if (fallback_value !== undefined) {
fallback_value = new_value;
@@ -384,7 +390,10 @@ export function prop(props, key, flags, fallback) {
return value;
}
- if (has_destroyed_component_ctx(d)) {
+ // special case — avoid recalculating the derived if
+ // we're in a teardown function and the prop
+ // was overridden locally
+ if (is_destroying_effect && overridden) {
return d.v;
}
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte
new file mode 100644
index 000000000000..3c863c7fc94c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte
@@ -0,0 +1,15 @@
+
+
+{count}
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js
new file mode 100644
index 000000000000..1f6e5d1f7f96
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js
@@ -0,0 +1,17 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [increment, toggle] = target.querySelectorAll('button');
+
+ flushSync(() => toggle.click());
+ assert.deepEqual(logs, [0, 'hello']);
+
+ flushSync(() => toggle.click());
+ flushSync(() => increment.click());
+ flushSync(() => increment.click());
+
+ assert.deepEqual(logs, [0, 'hello', 1, 'hello']);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte
new file mode 100644
index 000000000000..1a97d3e760d6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
+{#if count < 2 && message === 'hello'}
+
+{/if}
From c8ab9e610465efa6fa0b2b234c4bf4ceeeed7fd2 Mon Sep 17 00:00:00 2001
From: Rich Harris
Date: Tue, 1 Jul 2025 22:39:25 -0400
Subject: [PATCH 25/25] fix
---
.../src/internal/client/reactivity/props.js | 74 ++++++++++++++-----
1 file changed, 54 insertions(+), 20 deletions(-)
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index 33f0e67a6d1d..4771e786520f 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -1,5 +1,5 @@
/** @import { ComponentContext } from '#client' */
-/** @import { Derived, Source } from './types.js' */
+/** @import { Derived, Effect, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
@@ -12,9 +12,15 @@ import {
import { get_descriptor, is_function } from '../../shared/utils.js';
import { set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
-import { get, is_destroying_effect, untrack } from '../runtime.js';
+import {
+ active_effect,
+ get,
+ is_destroying_effect,
+ set_active_effect,
+ untrack
+} from '../runtime.js';
import * as e from '../errors.js';
-import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
+import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
@@ -94,7 +100,7 @@ export function rest_props(props, exclude, name) {
/**
* The proxy handler for legacy $$restProps and $$props
- * @type {ProxyHandler<{ props: Record, exclude: Array, special: Record unknown>, version: Source }>}}
+ * @type {ProxyHandler<{ props: Record, exclude: Array, special: Record unknown>, version: Source, parent_effect: Effect }>}}
*/
const legacy_rest_props_handler = {
get(target, key) {
@@ -104,17 +110,25 @@ const legacy_rest_props_handler = {
},
set(target, key, value) {
if (!(key in target.special)) {
- // Handle props that can temporarily get out of sync with the parent
- /** @type {Record unknown>} */
- target.special[key] = prop(
- {
- get [key]() {
- return target.props[key];
- }
- },
- /** @type {string} */ (key),
- PROPS_IS_UPDATED
- );
+ var previous_effect = active_effect;
+
+ try {
+ set_active_effect(target.parent_effect);
+
+ // Handle props that can temporarily get out of sync with the parent
+ /** @type {Record unknown>} */
+ target.special[key] = prop(
+ {
+ get [key]() {
+ return target.props[key];
+ }
+ },
+ /** @type {string} */ (key),
+ PROPS_IS_UPDATED
+ );
+ } finally {
+ set_active_effect(previous_effect);
+ }
}
target.special[key](value);
@@ -153,7 +167,19 @@ const legacy_rest_props_handler = {
* @returns {Record}
*/
export function legacy_rest_props(props, exclude) {
- return new Proxy({ props, exclude, special: {}, version: source(0) }, legacy_rest_props_handler);
+ return new Proxy(
+ {
+ props,
+ exclude,
+ special: {},
+ version: source(0),
+ // TODO this is only necessary because we need to track component
+ // destruction inside `prop`, because of `bind:this`, but it
+ // seems likely that we can simplify `bind:this` instead
+ parent_effect: /** @type {Effect} */ (active_effect)
+ },
+ legacy_rest_props_handler
+ );
}
/**
@@ -376,6 +402,12 @@ export function prop(props, key, flags, fallback) {
// Capture the initial value if it's bindable
if (bindable) get(d);
+ var parent_effect = /** @type {Effect} */ (active_effect);
+
+ if (!parent_effect) {
+ console.trace();
+ }
+
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
@@ -390,10 +422,12 @@ export function prop(props, key, flags, fallback) {
return value;
}
- // special case — avoid recalculating the derived if
- // we're in a teardown function and the prop
- // was overridden locally
- if (is_destroying_effect && overridden) {
+ // special case — avoid recalculating the derived if we're in a
+ // teardown function and the prop was overridden locally, or the
+ // component was already destroyed (this latter part is necessary
+ // because `bind:this` can read props after the component has
+ // been destroyed. TODO simplify `bind:this`
+ if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
return d.v;
}