Skip to content

Commit da4cd0e

Browse files
authored
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
1 parent 1bc3ae7 commit da4cd0e

File tree

4 files changed

+65
-11
lines changed

4 files changed

+65
-11
lines changed

.changeset/great-ghosts-unite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: link offscreen items and last effect in each block correctly

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,8 @@ function reconcile(state, array, anchor, flags, get_key) {
507507
current = item.next;
508508
}
509509

510+
let has_offscreen_items = items.size > length;
511+
510512
if (current !== null || seen !== undefined) {
511513
var to_destroy = seen === undefined ? [] : array_from(seen);
512514

@@ -520,6 +522,8 @@ function reconcile(state, array, anchor, flags, get_key) {
520522

521523
var destroy_length = to_destroy.length;
522524

525+
has_offscreen_items = items.size - destroy_length > length;
526+
523527
if (destroy_length > 0) {
524528
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
525529

@@ -537,6 +541,18 @@ function reconcile(state, array, anchor, flags, get_key) {
537541
}
538542
}
539543

544+
// Append offscreen items at the end
545+
if (has_offscreen_items) {
546+
for (const item of items.values()) {
547+
if (!item.o) {
548+
link(state, prev, item);
549+
prev = item;
550+
}
551+
}
552+
}
553+
554+
state.effect.last = prev && prev.e;
555+
540556
if (is_animated) {
541557
queue_micro_task(() => {
542558
if (to_animate === undefined) return;
@@ -641,10 +657,6 @@ function link(state, prev, next) {
641657
state.first = next;
642658
state.effect.first = next && next.e;
643659
} else {
644-
if (prev.e === state.effect.last && next !== null) {
645-
state.effect.last = next.e;
646-
}
647-
648660
if (prev.e.next) {
649661
prev.e.next.prev = null;
650662
}
@@ -653,13 +665,7 @@ function link(state, prev, next) {
653665
prev.e.next = next && next.e;
654666
}
655667

656-
if (next === null) {
657-
state.effect.last = prev && prev.e;
658-
} else {
659-
if (next.e === state.effect.last && prev === null) {
660-
state.effect.last = next.e.prev;
661-
}
662-
668+
if (next !== null) {
663669
if (next.e.prev) {
664670
next.e.prev.next = null;
665671
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [add4, add5, modify3] = target.querySelectorAll('button');
7+
8+
add4.click();
9+
flushSync();
10+
assert.htmlEqual(
11+
target.innerHTML,
12+
`<button>add 4</button> <button>add 5</button> <button>modify 3</button>
13+
1423`
14+
);
15+
16+
add5.click();
17+
flushSync();
18+
assert.htmlEqual(
19+
target.innerHTML,
20+
`<button>add 4</button> <button>add 5</button> <button>modify 3</button>
21+
14523`
22+
);
23+
24+
modify3.click();
25+
flushSync();
26+
assert.htmlEqual(
27+
target.innerHTML,
28+
`<button>add 4</button> <button>add 5</button> <button>modify 3</button>
29+
1452updated`
30+
);
31+
}
32+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
let list = $state([{ id: 1, text: '1' }, { id: 2, text: '2' }, { id: 3, text: '3' }]);
3+
</script>
4+
5+
<button onclick={() => list = [list[0], { id: 4, text: '4' }, ...list.slice(1)]}>add 4</button>
6+
<button onclick={() => list = [list[0], list[1], { id: 5, text: '5' }, ...list.slice(2)]}>add 5</button>
7+
<button onclick={() => list.at(-1).text = 'updated'}>modify 3</button>
8+
9+
{#each list as item (item.id)}
10+
{item.text}
11+
{/each}

0 commit comments

Comments
 (0)