diff --git a/.changeset/stale-keys-taste.md b/.changeset/stale-keys-taste.md new file mode 100644 index 0000000000..b1b02323bb --- /dev/null +++ b/.changeset/stale-keys-taste.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure unowned derived dependencies are not duplicated when reactions are skipped diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 57cefccc01..30f14b7356 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -188,7 +188,8 @@ export function check_dirtiness(reaction) { dependency = dependencies[i]; // We always re-add all reactions (even duplicates) if the derived was - // previously disconnected + // previously disconnected, however we don't if it was unowned as we + // de-duplicate dependencies in that case if (is_disconnected || !dependency?.reactions?.includes(reaction)) { (dependency.reactions ??= []).push(reaction); } @@ -404,15 +405,9 @@ export function update_reaction(reaction) { skipped_deps = 0; untracked_writes = null; active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; - // prettier-ignore skip_reaction = (flags & UNOWNED) !== 0 && - (!is_flushing_effect || - // If we were previously not in a reactive context and we're reading an unowned derived - // that was created inside another reaction, then we don't fully know the real owner and thus - // we need to skip adding any reactions for this unowned - ((previous_reaction === null || previous_untracking) && - /** @type {Derived} */ (reaction).parent !== null)); + (!is_flushing_effect || previous_reaction === null || previous_untracking); derived_sources = null; set_component_context(reaction.ctx); @@ -933,7 +928,10 @@ export function get(signal) { skipped_deps++; } else if (new_deps === null) { new_deps = [signal]; - } else { + } else if (!skip_reaction || !new_deps.includes(signal)) { + // Normally we can push duplicated dependencies to `new_deps`, but if we're inside + // an unowned derived because skip_reaction is true, then we need to ensure that + // we don't have duplicates new_deps.push(signal); } } diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index bd9bc50ae3..46209ede81 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -818,6 +818,28 @@ describe('signals', () => { }; }); + test('unowned deriveds dependencies are correctly de-duped', () => { + return () => { + let a = state(0); + let b = state(true); + let c = derived(() => $.get(a)); + let d = derived(() => ($.get(b) ? 1 : $.get(a) + $.get(c) + $.get(a))); + + $.get(d); + + assert.equal(d.deps?.length, 1); + + $.get(d); + + set(a, 1); + set(b, false); + + $.get(d); + + assert.equal(d.deps?.length, 3); + }; + }); + test('unowned deriveds correctly update', () => { return () => { const arr1 = proxy<{ a: number }[]>([]);