fix: lazy props reactivity#18146
Merged
Rich-Harris merged 7 commits intoApr 29, 2026
Merged
Conversation
🦋 Changeset detectedLatest commit: 53c67a2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
6 tasks
Rich-Harris
reviewed
Apr 29, 2026
Merged
Rich-Harris
pushed a commit
that referenced
this pull request
May 14, 2026
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## svelte@5.55.6 ### Patch Changes - fix: leave stale promises to wait for a later resolution, instead of rejecting ([#18180](#18180)) - fix: keep dependencies of `$state.eager/pending` ([#18218](#18218)) - fix: reapply context after transforming error during SSR ([#18099](#18099)) - fix: don't rebase just-created batches ([#18117](#18117)) - chore: allow `null` for `pending` in typings ([#18201](#18201)) - fix: flush eager effects in production ([#18107](#18107)) - fix: rethrow error of failed iterable after calling `return()` ([#18169](#18169)) - fix: account for proxified instance when updating `bind:this` ([#18147](#18147)) - fix: ensure scheduled batch is flushed if not obsolete ([#18131](#18131)) - fix: resolve stale deriveds with latest value ([#18167](#18167)) - chore: remove unnecessary `increment_pending` calls ([#18183](#18183)) - fix: correctly compile component member expressions for SSR ([#18192](#18192)) - fix: reset `source.updated` stack traces after `flush` ([#18196](#18196)) - fix: replacing async 'blocking' strategy with 'merging' ([#18205](#18205)) - fix: allow `@debug` tags to reference awaited variables ([#18138](#18138)) - fix: re-run fallback props if dependencies update ([#18146](#18146)) - fix: abort running obsolete async branches ([#18118](#18118)) - fix: ignore comments when reading CSS values ([#18153](#18153)) - fix: wrap `Promise.all` in `save` during SSR ([#18178](#18178)) - fix: ignore false-positive errors of `$inspect` dependencies ([#18106](#18106)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Fix #18132
This PR treat lazy fallbacks on
prop()as derived. Now a default function that uses a $state is recalculated whenever its dependents changes. This change implies that this lazy functions cannot mutate a state anymore (because it is derived), causing astate_unsafe_mutationerror. This implies on a breaking change, but reasonable.New breaking change here
Why make this breaking change
This encourages people to not update states on a function that fundamentaly, is readonly. When someone wants to use a default function expecting that it should be tracked, its not likely that this function will change some state. It is anti-pattern to change some state inside a getter function.
But what if someone wants to do it, like in the code above?
The code above doesn't make sense before this PR, the old way to calculate lazy functions is to execute it one time, and only one, so the
callCountvariable will never change. But let's assume that someone did it, how to migrate?The migration in same example is easy, since the
callCountis executed only once, it will not be executed after the component is mounted. So thecallCountdoesn't need to be a state, thecallCountwill be in a valid state when the component is created. So here is the migrated code:As we can see, there is no reason for the variable
callCountin this example (before this PR), and if someone did it, it is more likely that they used a constant instead:There is another example that causes the
state_unsafe_mutationand how to fix (this happened on the tests that i changed):Here, we can see that
logvariable is a state. Before this PR, as i said, this functionfallbackExamplewill be executed once. So the logs will be computed when the component is mounted. So there is no reason to make theloga state. The simplest way to fix this is to make it a normal variable:But with this PR, the function might be recalculated at some point, and the
logwith a state makes sense now, so how to migrate in this case? As i said, changing a state inside a lazy prop function is not a good practice, we can think in a way to invert this dependency, and change the approach from push (imperative mutation) to pull (declarative derivation).If a developer really needs to track how many times a fallback is executed or react to its changes, they should use a $derived or an $effect that observes the same dependencies as the fallback, or simply observe the property itself:
After all, how to migrate?
If there is no state mutation inside the prop function, no need to changes;
If there is a state mutation inside the prop function, but the value muted is declared inside the same component: remove the state from it. Before this PR the method will be executed only once, and to get the same result, you do not need the variable to be a state;
Before
After
Before:
After
Severity (number of people affected x effort): Low
Affected Users: Minimal. Mutating state inside a property initializer is a rare edge case and considered an anti-pattern (because its a side effect inside a getter). Most users use constants or pure functions for fallbacks.
Migration Effort: Low. As demonstrated in the examples above, the fix usually involves either removing an unnecessary $state or moving the side effect to its proper place, the $effect
Conclusion
This PR encourages users to program in a better way. Forcing a clean separation between data and their side effects. The developer can use this new feature mainly in i18n services, providing better usability and experience. Also, this PR makes the properties more predictable, since the expected behavior is that it works reactively, eliminating this bug for future developers.
Even though this PR adds a breaking change, it's easily solvable, and the chance of any user facing this problem is low.
Full example to test reactivity in props (won't work on web, you can get the PR and test localy to see it working): https://svelte.dev/playground/a6608434d8c642179f0e2b72468c74d7?version=latest
A unit test for this reactivity was created: runtime-runes/props-default-value-reactivity.