Skip to content

fix: ensure sources within nested effects still register correctly #16193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rich-emus-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: ensure sources within nested effects still register correctly
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function proxy(value) {
var reaction = active_reaction;

/**
* Executes the proxy in the context of the reaction it was originally created in, if any
* @template T
* @param {() => T} fn
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/reactivity/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function set(source, value, should_proxy = false) {
!untracking &&
is_runes() &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
!reaction_sources?.includes(source)
!(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction)
) {
e.state_unsafe_mutation();
}
Expand Down
12 changes: 6 additions & 6 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,18 @@ export function set_active_effect(effect) {

/**
* When sources are created within a reaction, reading and writing
* them should not cause a re-run
* @type {null | Source[]}
* them within that reaction should not cause a re-run
* @type {null | [active_reaction: Reaction, sources: Source[]]}
*/
export let reaction_sources = null;

/** @param {Value} value */
export function push_reaction_value(value) {
if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
if (reaction_sources === null) {
reaction_sources = [value];
reaction_sources = [active_reaction, [value]];
} else {
reaction_sources.push(value);
reaction_sources[1].push(value);
}
}
}
Expand Down Expand Up @@ -234,7 +234,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i];

if (reaction_sources?.includes(signal)) continue;
if (reaction_sources?.[1].includes(signal) && reaction_sources[0] === active_reaction) continue;

if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
Expand Down Expand Up @@ -724,7 +724,7 @@ export function get(signal) {

// Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) {
if (!reaction_sources?.includes(signal)) {
if (!reaction_sources?.[1].includes(signal) || reaction_sources[0] !== active_reaction) {
var deps = active_reaction.deps;
if (signal.rv < read_version) {
signal.rv = read_version;
Expand Down
35 changes: 35 additions & 0 deletions packages/svelte/tests/signals/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,41 @@ describe('signals', () => {
};
});

test('nested effects depend on state of upper effects', () => {
const logs: number[] = [];

user_effect(() => {
const raw = state(0);
const proxied = proxy({ current: 0 });

// We need those separate, else one working and rerunning the effect
// could mask the other one not rerunning
user_effect(() => {
logs.push($.get(raw));
});

user_effect(() => {
logs.push(proxied.current);
});

// Important so that the updating effect is not running
// together with the reading effects
flushSync();

user_effect(() => {
$.untrack(() => {
set(raw, $.get(raw) + 1);
proxied.current += 1;
});
});
});

return () => {
flushSync();
assert.deepEqual(logs, [0, 0, 1, 1]);
};
});

test('proxy version state does not trigger self-dependency guard', () => {
return () => {
const s = proxy({ a: { b: 1 } });
Expand Down