Skip to content

Commit 7c321a4

Browse files
committed
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
1 parent c6f99e6 commit 7c321a4

File tree

4 files changed

+88
-0
lines changed

4 files changed

+88
-0
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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,8 @@ function reconcile(state, array, anchor, flags, get_key) {
503503
current = item.next;
504504
}
505505

506+
let has_offscreen_items = items.size > length;
507+
506508
if (current !== null || seen !== undefined) {
507509
var to_destroy = seen === undefined ? [] : array_from(seen);
508510

@@ -516,6 +518,8 @@ function reconcile(state, array, anchor, flags, get_key) {
516518

517519
var destroy_length = to_destroy.length;
518520

521+
has_offscreen_items = items.size - destroy_length > length;
522+
519523
if (destroy_length > 0) {
520524
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
521525

@@ -533,6 +537,18 @@ function reconcile(state, array, anchor, flags, get_key) {
533537
}
534538
}
535539

540+
// Append offscreen items at the end
541+
if (has_offscreen_items) {
542+
for (const item of items.values()) {
543+
if (!item.o) {
544+
link(state, prev, item);
545+
prev = item;
546+
}
547+
}
548+
}
549+
550+
state.effect.last = prev && prev.e;
551+
536552
if (is_animated) {
537553
queue_micro_task(() => {
538554
if (to_animate === undefined) return;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [add, adjust] = target.querySelectorAll('button');
7+
8+
add.click();
9+
flushSync();
10+
assert.htmlEqual(
11+
target.innerHTML,
12+
`<button>add</button> <button>adjust</button>
13+
<h2>Keyed</h2>
14+
<div>Item: 1. Index: 0</div>
15+
<div>Item: 0. Index: 1</div>
16+
<h2>Unkeyed</h2>
17+
<div>Item: 1. Index: 0</div>
18+
<div>Item: 0. Index: 1</div>`
19+
);
20+
21+
add.click();
22+
flushSync();
23+
assert.htmlEqual(
24+
target.innerHTML,
25+
`<button>add</button> <button>adjust</button>
26+
<h2>Keyed</h2>
27+
<div>Item: 2. Index: 0</div>
28+
<div>Item: 1. Index: 1</div>
29+
<div>Item: 0. Index: 2</div>
30+
<h2>Unkeyed</h2>
31+
<div>Item: 2. Index: 0</div>
32+
<div>Item: 1. Index: 1</div>
33+
<div>Item: 0. Index: 2</div>`
34+
);
35+
36+
adjust.click();
37+
flushSync();
38+
assert.htmlEqual(
39+
target.innerHTML,
40+
`<button>add</button> <button>adjust</button>
41+
<h2>Keyed</h2>
42+
<div>Item: 2. Index: 0</div>
43+
<div>Item: 1. Index: 1</div>
44+
<div>Item: 10. Index: 2</div>
45+
<h2>Unkeyed</h2>
46+
<div>Item: 2. Index: 0</div>
47+
<div>Item: 1. Index: 1</div>
48+
<div>Item: 10. Index: 2</div>`
49+
);
50+
}
51+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
const items = $state([{ t: 0 }]);
3+
</script>
4+
5+
<button onclick={() => items.unshift({t:items.length})}>add</button>
6+
<button onclick={() => items.at(-1).t = 10}>adjust</button>
7+
8+
<h2>Keyed</h2>
9+
{#each items as item, index (item)}
10+
<div>Item: {item.t}. Index: {index}</div>
11+
{/each}
12+
13+
<h2>Unkeyed</h2>
14+
{#each items as item, index}
15+
<div>Item: {item.t}. Index: {index}</div>
16+
{/each}

0 commit comments

Comments
 (0)