fix: top-most derived in a chain of deriveds marked as MAYBE_DIRTY when executed from a snippet $.fallback #16111
+104
−2
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes: #16090
The Problem
A chain of derived signals fails to update if an intermediate signal in the chain is also read inside a
{#snippet}
that has a parameter with a default value.The root cause is a subtle interaction between how the snippet's fallback value is evaluated and how the reactivity system determines if a signal is "clean" or "dirty". This leads to an inconsistent state where the intermediate derived signal gets "stuck" and no longer propagates updates.
Detailed Breakdown
Compiler Generates a Fallback Reaction
The entire issue originates from the code the Svelte compiler generates for a snippet with a default parameter. A snippet like
{#snippet dummy(value = 0)}
is compiled into a structure that uses a$.fallback()
call. This generated code then immediately reads the derived values within a special context.The compiled output looks like this:
When
$.get(value)
is executed, it runs in a context where askip_reaction
flag is set totrue
. This is the trigger for the entire bug.Initial Render: An Inconsistent State is Created
When the
$.get(value)
from the compiled snippet runs, it reads bothderived1
andderived2
. Becauseskip_reaction
istrue
, the following happens:First,
derived1
is evaluated. Theupdate_derived
function sees thatskip_reaction
is true and incorrectly marksderived1
asMAYBE_DIRTY
.Code from
packages/svelte/src/internal/client/reactivity/deriveds.js
:Next,
derived2
is evaluated. Thecheck_dirtiness
function is called for it. Crucially, at the end of this function,derived2
is marked asCLEAN
because it's being accessed within an active effect whereskip_reaction
does not prevent the cleaning.Code from
packages/svelte/src/internal/client/runtime.js
:This creates an inconsistent state:
derived1
isMAYBE_DIRTY
while its dependent,derived2
, isCLEAN
.Update Trigger: The Reactivity Chain is Broken
Later, the
override
state is changed. This correctly callsmark_reactions
on its dependencies, includingderived1
.Code from
packages/svelte/src/internal/client/reactivity/sources.js
:Because
derived1
was stuck in theMAYBE_DIRTY
state, the crucial check to continue the reaction chain fails.derived2
is never notified that it is stale.Result: Stale UI
When Svelte flushes the effects to update the DOM, it sees that
derived2
is stillCLEAN
and therefore does not re-render it. The UI is stuck showing the old value.The Fix
The fix is to remove the
skip_reaction
check from the status calculation withinupdate_derived
. This ensures a derived signal's status is determined only by its own properties (UNOWNED
anddeps
), preventing an unrelated context flag from corrupting its state.Code change in
packages/svelte/src/internal/client/reactivity/deriveds.js
:This ensures
derived1
is correctly markedCLEAN
in the first step. As a result, when its source changes, the(flags & CLEAN) !== 0
check inmark_reactions
passes, the reactivity chain remains intact, andderived2
updates as expected.Disclaimer
It is important to note that the
skip_reaction
flag has been part of this status calculation for a significant time. Therefore, while this fix does resolve the observed bug, I am not completely certain it is the ideal solution, as I do not fully understand the original intent behind includingskip_reaction
in this specific logic. Removing the flag did not cause any existing tests to fail.Furthermore, I was unable to create a test for this specific issue that fails without the fix and passes with it. The bug is consistently reproducible in a live browser environment, but it does not manifest in the JSDOM-based test runner.
Before submitting the PR, please make sure you do the following
feat:
,fix:
,chore:
, ordocs:
.packages/svelte/src
, add a changeset (npx changeset
).Tests and linting
pnpm test
and lint the project withpnpm lint