diff --git a/.changeset/lemon-weeks-call.md b/.changeset/lemon-weeks-call.md new file mode 100644 index 000000000000..ae62305630b5 --- /dev/null +++ b/.changeset/lemon-weeks-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't destroy effect roots created inside of deriveds diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c4edd2bf8d95..f44efa32f1ca 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -148,7 +148,11 @@ function create_effect(type, fn, sync, push = true) { } // if we're in a derived, add the effect there too - if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) { + if ( + active_reaction !== null && + (active_reaction.f & DERIVED) !== 0 && + (type & ROOT_EFFECT) === 0 + ) { var derived = /** @type {Derived} */ (active_reaction); (derived.effects ??= []).push(effect); } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 72187e84a720..81f7197b806d 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -54,7 +54,7 @@ export interface Reaction extends Signal { export interface Derived extends Value, Reaction { /** The derived function */ fn: () => V; - /** Effects created inside this signal */ + /** Effects created inside this signal. Used to destroy those effects when the derived reruns or is cleaned up */ effects: null | Effect[]; /** Parent effect or derived */ parent: Effect | Derived | null; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 937324727b16..eff6d6166a5e 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1390,4 +1390,55 @@ describe('signals', () => { destroy(); }; }); + + test('$effect.root inside deriveds stay alive independently', () => { + const log: any[] = []; + const c = state(0); + const cleanup: any[] = []; + const inner_states: any[] = []; + + const d = derived(() => { + const destroy = effect_root(() => { + const x = state(0); + inner_states.push(x); + + effect(() => { + log.push('inner ' + $.get(x)); + return () => { + log.push('inner destroyed'); + }; + }); + }); + + cleanup.push(destroy); + + return $.get(c); + }); + + return () => { + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [0, 'inner 0']); + log.length = 0; + + set(inner_states[0], 1); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner 1']); + log.length = 0; + + set(c, 1); + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [1, 'inner 0']); + log.length = 0; + + cleanup.forEach((fn) => fn()); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner destroyed']); + }; + }); });