fix: top-most derived in a chain of deriveds marked as MAYBE_DIRTY when executed from a snippet $.fallback #16111
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_reactionflag 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 bothderived1andderived2. Becauseskip_reactionistrue, the following happens:First,
derived1is evaluated. Theupdate_derivedfunction sees thatskip_reactionis true and incorrectly marksderived1asMAYBE_DIRTY.Code from
packages/svelte/src/internal/client/reactivity/deriveds.js:Next,
derived2is evaluated. Thecheck_dirtinessfunction is called for it. Crucially, at the end of this function,derived2is marked asCLEANbecause it's being accessed within an active effect whereskip_reactiondoes not prevent the cleaning.Code from
packages/svelte/src/internal/client/runtime.js:This creates an inconsistent state:
derived1isMAYBE_DIRTYwhile its dependent,derived2, isCLEAN.Update Trigger: The Reactivity Chain is Broken
Later, the
overridestate is changed. This correctly callsmark_reactionson its dependencies, includingderived1.Code from
packages/svelte/src/internal/client/reactivity/sources.js:Because
derived1was stuck in theMAYBE_DIRTYstate, the crucial check to continue the reaction chain fails.derived2is never notified that it is stale.Result: Stale UI
When Svelte flushes the effects to update the DOM, it sees that
derived2is stillCLEANand therefore does not re-render it. The UI is stuck showing the old value.The Fix
The fix is to remove the
skip_reactioncheck from the status calculation withinupdate_derived. This ensures a derived signal's status is determined only by its own properties (UNOWNEDanddeps), preventing an unrelated context flag from corrupting its state.Code change in
packages/svelte/src/internal/client/reactivity/deriveds.js:This ensures
derived1is correctly markedCLEANin the first step. As a result, when its source changes, the(flags & CLEAN) !== 0check inmark_reactionspasses, the reactivity chain remains intact, andderived2updates as expected.Disclaimer
It is important to note that the
skip_reactionflag 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_reactionin 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 testand lint the project withpnpm lint