From adba758067e0e68f9fbdcd72a6103437ec061a27 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Mon, 6 Apr 2026 16:52:29 +0200
Subject: [PATCH] fix: don't reset status of uninitialized deriveds (#18054)
If a new branch is created (e.g. if block becomes truthy) inside a fork
and a derived is read inside the new branch for the first time, it will
get reactions but its true value will stay uninitialized due to the
fork. If the fork then commits, it will NOT change its value because it
will not reexecute: there is no effect running after commit telling it
to do that, because it was only read inside new effects which already
ran while still inside the fork.
Now, when that branch is removed (e.g. if block becomes falsy), the
derived loses its reactions again and becomes disconnected, at which
point the status is reset. So we end up with a maybe_dirty or clean
derived and an uninitialized value, causing breakage if it's called
again afterwards.
The fix is to not reset the status in this case.
Fixes https://github.com/sveltejs/kit/issues/15126
Fixes https://github.com/sveltejs/kit/issues/15318
Fixes https://github.com/sveltejs/kit/issues/15061
---
.changeset/silent-rings-yell.md | 5 +++++
.../svelte/src/internal/client/runtime.js | 9 ++++++++-
.../samples/async-fork-if-derived/_config.js | 20 +++++++++++++++++++
.../samples/async-fork-if-derived/main.svelte | 13 ++++++++++++
4 files changed, 46 insertions(+), 1 deletion(-)
create mode 100644 .changeset/silent-rings-yell.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte
diff --git a/.changeset/silent-rings-yell.md b/.changeset/silent-rings-yell.md
new file mode 100644
index 0000000000..0b417103f7
--- /dev/null
+++ b/.changeset/silent-rings-yell.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't reset status of uninitialized deriveds
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 906d68fbf0..d9578142eb 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -399,7 +399,14 @@ function remove_reaction(signal, dependency) {
derived.f &= ~WAS_MARKED;
}
- update_derived_status(derived);
+ // In a fork it's possible that a derived is executed and gets reactions, then commits, but is
+ // never re-executed. This is possible when the derived is only executed once in the context
+ // of a new branch which happens before fork.commit() runs. In this case, the derived still has
+ // UNINITIALIZED as its value, and then when it's loosing its reactions we need to ensure it stays
+ // DIRTY so it is reexecuted once someone wants its value again.
+ if (derived.v !== UNINITIALIZED) {
+ update_derived_status(derived);
+ }
// freeze any effects inside this derived
freeze_derived_effects(derived);
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js
new file mode 100644
index 0000000000..4a03a54e7a
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js
@@ -0,0 +1,20 @@
+import { tick } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [, toggle] = target.querySelectorAll('button');
+
+ toggle?.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, ` 0`);
+
+ toggle?.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, ` `);
+
+ toggle?.click();
+ await tick();
+ assert.htmlEqual(target.innerHTML, ` 0`);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte
new file mode 100644
index 0000000000..519857a9ce
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
+{#if show}
+ {d_count}
+{/if}