From da4cd0e35942ba9615ba6516ffaf185b511e7b59 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Wed, 26 Nov 2025 20:06:15 +0100
Subject: [PATCH] fix: link offscreen items and last effect in each block
correctly (#17240)
* fix: link offscreen items and last effect in each block correctly
It's possible that due to how new elements are inserted into the array that `effect.last` is wrong. We need to ensure it is really the last item to keep items properly connected to the graph. In addition we link offscreen items after all onscreen items, to ensure they don't have wrong pointers.
Fixes #17201
* revert #17244
* add test
---
.changeset/great-ghosts-unite.md | 5 +++
.../src/internal/client/dom/blocks/each.js | 28 +++++++++-------
.../samples/each-updates-11/_config.js | 32 +++++++++++++++++++
.../samples/each-updates-11/main.svelte | 11 +++++++
4 files changed, 65 insertions(+), 11 deletions(-)
create mode 100644 .changeset/great-ghosts-unite.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte
diff --git a/.changeset/great-ghosts-unite.md b/.changeset/great-ghosts-unite.md
new file mode 100644
index 0000000000..2973737cfd
--- /dev/null
+++ b/.changeset/great-ghosts-unite.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: link offscreen items and last effect in each block correctly
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 0147de3c3d..501577053d 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -507,6 +507,8 @@ function reconcile(state, array, anchor, flags, get_key) {
current = item.next;
}
+ let has_offscreen_items = items.size > length;
+
if (current !== null || seen !== undefined) {
var to_destroy = seen === undefined ? [] : array_from(seen);
@@ -520,6 +522,8 @@ function reconcile(state, array, anchor, flags, get_key) {
var destroy_length = to_destroy.length;
+ has_offscreen_items = items.size - destroy_length > length;
+
if (destroy_length > 0) {
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
@@ -537,6 +541,18 @@ function reconcile(state, array, anchor, flags, get_key) {
}
}
+ // Append offscreen items at the end
+ if (has_offscreen_items) {
+ for (const item of items.values()) {
+ if (!item.o) {
+ link(state, prev, item);
+ prev = item;
+ }
+ }
+ }
+
+ state.effect.last = prev && prev.e;
+
if (is_animated) {
queue_micro_task(() => {
if (to_animate === undefined) return;
@@ -641,10 +657,6 @@ function link(state, prev, next) {
state.first = next;
state.effect.first = next && next.e;
} else {
- if (prev.e === state.effect.last && next !== null) {
- state.effect.last = next.e;
- }
-
if (prev.e.next) {
prev.e.next.prev = null;
}
@@ -653,13 +665,7 @@ function link(state, prev, next) {
prev.e.next = next && next.e;
}
- if (next === null) {
- state.effect.last = prev && prev.e;
- } else {
- if (next.e === state.effect.last && prev === null) {
- state.effect.last = next.e.prev;
- }
-
+ if (next !== null) {
if (next.e.prev) {
next.e.prev.next = null;
}
diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js
new file mode 100644
index 0000000000..a8782d2da8
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/each-updates-11/_config.js
@@ -0,0 +1,32 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ async test({ assert, target }) {
+ const [add4, add5, modify3] = target.querySelectorAll('button');
+
+ add4.click();
+ flushSync();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ 1423`
+ );
+
+ add5.click();
+ flushSync();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ 14523`
+ );
+
+ modify3.click();
+ flushSync();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+ 1452updated`
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte
new file mode 100644
index 0000000000..1dcd265093
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/each-updates-11/main.svelte
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+{#each list as item (item.id)}
+ {item.text}
+{/each}