Repository: sveltejs/svelte (JavaScript)

Issue:
Outro transition gets interrupted when mutating array
### Describe the bug

mutating an array in Svelte 5 causes the outro animation to end abruptly and the element to be removed immediately from the DOM.

in Svelte 4 it works correctly: [REPL](https://svelte.dev/repl/b0b36a3a0c084f7e830afcfd176cd39a?version=4.2.19)

### Reproduction

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACm1RwWrdMBD8lUUtxA7m-eXQi-v3oJfcQkKaW5yDaq1jUVky0iptEPr3ohVt09CLjWZnZmd3k1i0wSCGxySs3FAM4su-i07Q614e4QUNoehEcNHPBRnD7PVO58lO1F9C-U30lWnwCW4jwYOXNmjSzsI97t6pOJfHoVIf_CvsMazaPoOEBX-AJtwCSKuAVrTgcXMvXLVc6ipcNL8htrrsy1dvu_MECRapEDIs3m1wUWP39CfJxWfubicySGB0IDjBx0CSsHl8akvVTrREy1G5WdNCqpEL_cBQAq0GuJG0Hry0ym1N2wHhTxrY82DQPtMKuS3C_M6UB8Nr7QO98w670TM2xw6uOEqRjv3fRdvxWyRyFpydjZ6_n1JJk_kGd2Uv6U131lb--T_KNymqwT0DwAhv9x_9ZMdomJc-oJzXujsZmAkNX0OrNtdpRqPBRRrKMU4pgYpeltkHuDoej5DzObGirCyPvdHVuS_OnDuas-jE5pReNCoxkI-Yn_Ivj4K48acCAAA=

### Logs

_No response_

### System Info

```shell
irrelevant
```


### Severity

annoyance


Discussion:
Has this been solved? When I try it in each of the provided links I can't spot a difference. Or maybe I'm doing it wrong? I'm just clicking the buttons to manipulate the array, but keep getting the expected behavior with transitions.
So digging into this more, the issue is:

https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/internal/client/reactivity/effects.js#L485-L486

When we remove the first item, that row because inert. When we add a new row we reconcile again, but the first item is still flagged for removal again, so we try to pause it again. However, as it's now inert, we don't add it to the transition array and just wipe it instantly without waiting for it.
@trueadm thanks for the quick fix! although the issue still occurs when mutating an empty array (adding elements to the array once it's empty or just setting array = [] still interrupts the transitions)

repro: https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACm1RS0_EIBD-KxM0cWuadj14qd1NPHowGvW23QO2U0uk0MDgI4T_boCN7wsE5nvMN-PZKCRa1uw8U3xG1rDLZWElo_clPuwLSkJWMqud6eNPa3sjFtp2qqP6FOLV0X2CwTncOIIHw5UVJLSCO1yMHlwfH1WGPph36KXon4V6ApoQOnaHs35B4FKCIJxtx-DREWkV6woWZ6cI5iqVy8SiTxMLr0JK4I_a0MHjxmSL_7XpVfSYkKd1PMW8aEPgYeQDQoDR6BlOcvL6y-fkIomrjiQSSGEJNnBsiROudvsiVlVHo1Mpbep6VYDPHUV4lb48iKGBa05TZbga9LwqSiB8oyZpVhLVE00QikgMv0RNSnMp5VXM8lMeNrDbXxxYbf21JtUepqlVmsrGx0ZC2uCtsxP4b8aJm_Hbf5g_G8gaf0b8XaFTrZMJ5o-Q91MeHLcJCqt4VmIoQs7RSgHaURM3sfEeBmd4DN7A2Xq9hhC2PjHivEJbS5GV66icOndyy0o260GMAgfWkHEY9uEDWnG_IOcCAAA=



Patch:
diff --git a/.changeset/unlucky-baboons-greet.md b/.changeset/unlucky-baboons-greet.md
new file mode 100644
index 000000000000..f720e64abff7
--- /dev/null
+++ b/.changeset/unlucky-baboons-greet.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: improve reconciliation of inert each block rows
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 5753c7cb0221..d09d2dac5cfe 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -373,7 +373,11 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
 			stashed = [];
 
 			while (current !== null && current.k !== key) {
-				(seen ??= new Set()).add(current);
+				// If the item has an effect that is already inert, skip over adding it
+				// to our seen Set as the item is already being handled
+				if ((current.e.f & INERT) === 0) {
+					(seen ??= new Set()).add(current);
+				}
 				stashed.push(current);
 				current = current.next;
 			}


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-each/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-each/_config.js
new file mode 100644
index 000000000000..0dbd62e0ea60
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-each/_config.js
@@ -0,0 +1,34 @@
+import { flushSync } from '../../../../src/index-client.js';
+import { test } from '../../test';
+
+export default test({
+	test({ assert, raf, target }) {
+		const [btn1, btn2] = target.querySelectorAll('button');
+
+		btn1?.click();
+		btn1?.click();
+		btn1?.click();
+		flushSync();
+		assert.htmlEqual(
+			target.innerHTML,
+			'<button>Push</button><button>Remove</button><ul><li>0</li><li>1</li><li>2</li></ul'
+		);
+
+		btn2?.click();
+		flushSync();
+		raf.tick(50);
+
+		const li = /** @type {HTMLElement & { foo: number }} */ (target.querySelector('ul > li'));
+
+		assert.equal(li.foo, 0.5);
+
+		btn1?.click();
+		flushSync();
+
+		assert.equal(li.foo, 0.5);
+		assert.htmlEqual(
+			target.innerHTML,
+			'<button>Push</button><button>Remove</button><ul><li>0</li><li>1</li><li>2</li><li>3</li></ul'
+		);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-each/main.svelte
new file mode 100644
index 000000000000..8da2c8d5610d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/transition-each/main.svelte
@@ -0,0 +1,30 @@
+<script>
+	function foo(node, params) {
+		return {
+			duration: 100,
+			tick: (t, u) => {
+				node.foo = t;
+			}
+		};
+	}
+
+	let list = $state([]);
+	let id = 0;
+
+	function push() {
+		list.push({ id: id++ })
+	}
+
+	function removeFirst() {
+		list.splice(0, 1);
+	}
+</script>
+
+<button onclick={push}>Push</button>
+<button onclick={removeFirst}>Remove</button>
+
+<ul>
+	{#each list as item (item.id)}
+		<li out:foo>{item.id}</li>
+	{/each}
+</ul>