Repository: sveltejs/svelte (JavaScript)

Issue:
Nested effect runs twice on state update when wrapped in template effect
### Describe the bug

```svelte
<script>
    let count = $state(0);

    function increment() {
        count += 1;
    }

    $effect(() => {
        const doubled = count * 2;
        $effect(() => {
                //This nested effect is destroyed and recreated everytime count changes. So we log count to the console ONCE every update
            console.log({count, doubled});
        })
    })
</script>

<button onclick={increment}>
    Increment
</button>
```
In code above the nested effect and it's parent both track the same piece of state and as expected the code in the nested effect executes once everytime count is updated. 

But if we replace the parent effect with a template effect:
```svelte
<script>
    let count = $state(0);

    function increment() {
        count += 1;
    }
</script>

<button onclick={increment}>
    clicks: {count}
</button>

<p>{(() => {
    const doubled = count * 2;
    $effect(() => {
        //Logs twice on every update to count, in the second log doubled is always 0
        console.log({count, doubled})
    })
        return count
})()}</p>
```
I found that the code in the nested effect executes twice for every update to count. In the first run doubled is always up to date but the second run references a stale closure so doubled is always 0. I'm guessing that the effect that references the stale closure somehow escapes destruction, so this is a memory leak? Or is it a feature of template effects that I'm not aware of

### Reproduction

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAA22RTY-cMAyG_4oVrbTQomHb43xJq96qqj30WHrIBEPTzTgocdgdofz3Csgwg7bJgeD4tZ33GUSjDXqx_TUIkmcUW_HcdaIQfOnGH9-jYRSF8DY4NUb2Xjnd8bEiAACDDMoGYjjAg2fJmD3lu4rm2yaQYm0JNCmHZyTOchjmu3HNyo8H-LSbg7Gifbk0qGh_CsyWwJIyWr0chqVQTANMcb-FYao16WfNrO-OQ5blcDhe2ypLnqG24WSwhkMa4QN8ThM8YNOg4rVoXGX5zbYe-FUrBEuAPboLhK6WjMB2LlSAJuA_CB6VpRqMbZde2oM0r_Li4eneAfLW4MbYNhtSiSSIefIkv6U75OBo7lVRzLM87svuKApxtrVuNNZiyy5gLBacHTqvPW8WkonsX39PFd866_gGLKkyTZoTshF1L03AG-rpdqTNY0Ia7upZe81fM9cNZMnkDTupXjS1Wb5K-Q-Fim_73rNKOHSBKpHGWHZZfrHB1PTIEDyCsUoaz9bJFgs4oZJj1EuqT_ZNU7sWf_354_vGs9PU6uaSTa9YN4j57jZvfAdoUqRoLNLBL4b098-9etpf88dvrCi-p_o7_gPvdLCasAMAAA==

### Logs

_No response_

### System Info

```shell
System:
    OS: Linux 6.8 Linux Mint 22 (Wilma)
    CPU: (4) x64 Intel(R) Core(TM) i5-6300U CPU @ 2.40GHz
    Memory: 1.86 GB / 7.64 GB
    Container: Yes
    Shell: 5.2.21 - /bin/bash
  Binaries:
    Node: 20.17.0 - ~/.nvm/versions/node/v20.17.0/bin/node
    npm: 10.8.2 - ~/.nvm/versions/node/v20.17.0/bin/npm
    pnpm: 9.10.0 - ~/.nvm/versions/node/v20.17.0/bin/pnpm
```


### Severity

annoyance


Discussion:
This
https://github.com/sveltejs/svelte/blob/501f4151909077020f26135a90425bf175b03c1d/packages/svelte/src/internal/client/reactivity/effects.js#L189-L208
should be the responsible (and also the fact that `template_effects` are sync)...what's your specific use case?
>should be the responsible (and also the fact that template_effects are sync)...what's your specific use case?

I was experimenting different ways of writing an abstraction for persisting state in localStorage
```js
function persist(key, init) {
  let value = $state(JSON.parse(localStorage.getItem(key)) ?? init);
  return {
     get value() {
        if ($effect.tracking()) {
          $effect(() => localStorage.setItem(key, JSON.stringify(value)));
        }
        return value
     },
     set value(v) {
       value = v
     }
  }
}
```
And then in the component
```svelte
<script>
	import {persist} from "./persist.svelte.js";

	const items = persist([]);

	function increment() {
		items.value.push(items.value.length + 1)
	}
	
</script>

<button onclick={increment}>
	click: {items.value.reduce((sum, current) => sum + current, 0)}
</button>
```
The code above writes to localStorage twice on every update. I know there are better ways to write this but it just felt weird that the nested effect ran twice

You should keep track of how many effects are registered and only register a new one if that number is 0
Nice catch, I'll dig into this more.



Patch:
diff --git a/.changeset/khaki-camels-punch.md b/.changeset/khaki-camels-punch.md
new file mode 100644
index 000000000000..ee761ccc7ec7
--- /dev/null
+++ b/.changeset/khaki-camels-punch.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ensure correct parent effect is associated with render effects
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 225137ec1ec7..fa43f0afb25c 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -201,7 +201,10 @@ export function user_effect(fn) {
 
 	if (defer) {
 		var context = /** @type {ComponentContext} */ (component_context);
-		(context.e ??= []).push(fn);
+		(context.e ??= []).push({
+			fn,
+			parent: active_effect
+		});
 	} else {
 		var signal = effect(fn);
 		return signal;
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index ded38a67646f..bfea047b24ef 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -1050,11 +1050,18 @@ export function pop(component) {
 		if (component !== undefined) {
 			context_stack_item.x = component;
 		}
-		const effects = context_stack_item.e;
-		if (effects !== null) {
+		const component_effects = context_stack_item.e;
+		if (component_effects !== null) {
+			var previous_effect = active_effect;
 			context_stack_item.e = null;
-			for (var i = 0; i < effects.length; i++) {
-				effect(effects[i]);
+			try {
+				for (var i = 0; i < component_effects.length; i++) {
+					var component_effect = component_effects[i];
+					set_active_effect(component_effect.parent);
+					effect(component_effect.fn);
+				}
+			} finally {
+				set_active_effect(previous_effect);
 			}
 		}
 		component_context = context_stack_item.p;
diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts
index 9a3955d9ca4d..6d7065f1c876 100644
--- a/packages/svelte/src/internal/client/types.d.ts
+++ b/packages/svelte/src/internal/client/types.d.ts
@@ -15,7 +15,7 @@ export type ComponentContext = {
 	/** context */
 	c: null | Map<unknown, unknown>;
 	/** deferred effects */
-	e: null | Array<() => void | (() => void)>;
+	e: null | Array<{ fn: () => void | (() => void); parent: null | Effect }>;
 	/** mounted */
 	m: boolean;
 	/**


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-4/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-4/_config.js
new file mode 100644
index 000000000000..c1480c19a71d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-order-4/_config.js
@@ -0,0 +1,19 @@
+import { test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+	async test({ assert, target, logs }) {
+		const [b1] = target.querySelectorAll('button');
+		flushSync(() => {
+			b1.click();
+		});
+		flushSync(() => {
+			b1.click();
+		});
+		assert.deepEqual(logs, [
+			{ count: 0, doubled: 0 },
+			{ count: 1, doubled: 2 },
+			{ count: 2, doubled: 4 }
+		]);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-4/main.svelte
new file mode 100644
index 000000000000..31bd1f66682f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/effect-order-4/main.svelte
@@ -0,0 +1,18 @@
+<script>
+	let count = $state(0);
+
+	function increment() {
+		count += 1;
+	}
+
+	$effect.pre(() => {
+		const doubled = count * 2;
+		$effect(() => {
+			console.log({ count, doubled });
+		});
+	});
+</script>
+
+<button onclick={increment}>
+	clicks: {count}
+</button>