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}