Skip to content

Commit e365890

Browse files
authored
fix: maintain correct linked list of effects when updating each blocks (#17191)
* fix: maintain correct linked list of effects when updating each blocks * working * tidy up * changeset * remove unnecessary assignment, add comment explaining unidirectionality
1 parent 92c936d commit e365890

File tree

5 files changed

+104
-26
lines changed

5 files changed

+104
-26
lines changed

.changeset/upset-eyes-relax.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: maintain correct linked list of effects when updating each blocks

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

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,11 @@ function pause_effects(state, to_destroy, controlled_anchor) {
129129
export function each(node, flags, get_collection, get_key, render_fn, fallback_fn = null) {
130130
var anchor = node;
131131

132-
/** @type {EachState} */
133-
var state = { flags, items: new Map(), first: null };
132+
/** @type {Map<any, EachItem>} */
133+
var items = new Map();
134+
135+
/** @type {EachItem | null} */
136+
var first = null;
134137

135138
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
136139
var is_reactive_value = (flags & EACH_ITEM_REACTIVE) !== 0;
@@ -166,7 +169,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
166169
var first_run = true;
167170

168171
function commit() {
169-
reconcile(each_effect, array, state, anchor, flags, get_key);
172+
reconcile(state, array, anchor, flags, get_key);
170173

171174
if (fallback !== null) {
172175
if (array.length === 0) {
@@ -177,7 +180,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
177180
resume_effect(fallback.effect);
178181
}
179182

180-
each_effect.first = fallback.effect;
183+
effect.first = fallback.effect;
181184
} else {
182185
pause_effect(fallback.effect, () => {
183186
// TODO only null out if no pending batch needs it,
@@ -189,7 +192,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
189192
}
190193
}
191194

192-
var each_effect = block(() => {
195+
var effect = block(() => {
193196
array = /** @type {V[]} */ (get(each_array));
194197
var length = array.length;
195198

@@ -230,7 +233,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
230233
var value = array[i];
231234
var key = get_key(value, i);
232235

233-
var item = first_run ? null : state.items.get(key);
236+
var item = first_run ? null : items.get(key);
234237

235238
if (item) {
236239
// update before reconciliation, to trigger any async updates
@@ -263,15 +266,15 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
263266
item.o = true;
264267

265268
if (prev === null) {
266-
state.first = item;
269+
first = item;
267270
} else {
268271
prev.next = item;
269272
}
270273

271274
prev = item;
272275
}
273276

274-
state.items.set(key, item);
277+
items.set(key, item);
275278
}
276279

277280
keys.add(key);
@@ -302,7 +305,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
302305

303306
if (!first_run) {
304307
if (defer) {
305-
for (const [key, item] of state.items) {
308+
for (const [key, item] of items) {
306309
if (!keys.has(key)) {
307310
batch.skipped_effects.add(item.e);
308311
}
@@ -331,6 +334,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
331334
get(each_array);
332335
});
333336

337+
/** @type {EachState} */
338+
var state = { effect, flags, items, first };
339+
334340
first_run = false;
335341

336342
if (hydrating) {
@@ -341,15 +347,14 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
341347
/**
342348
* Add, remove, or reorder items output by an each block as its input changes
343349
* @template V
344-
* @param {Effect} each_effect
345-
* @param {Array<V>} array
346350
* @param {EachState} state
351+
* @param {Array<V>} array
347352
* @param {Element | Comment | Text} anchor
348353
* @param {number} flags
349354
* @param {(value: V, index: number) => any} get_key
350355
* @returns {void}
351356
*/
352-
function reconcile(each_effect, array, state, anchor, flags, get_key) {
357+
function reconcile(state, array, anchor, flags, get_key) {
353358
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
354359

355360
var length = array.length;
@@ -536,16 +541,6 @@ function reconcile(each_effect, array, state, anchor, flags, get_key) {
536541
}
537542
});
538543
}
539-
540-
// TODO i have an inkling that the rest of this function is wrong...
541-
// the offscreen items need to be linked, so that they all update correctly.
542-
// the last onscreen item should link to the first offscreen item, etc
543-
each_effect.first = state.first && state.first.e;
544-
each_effect.last = prev && prev.e;
545-
546-
if (prev) {
547-
prev.e.next = null;
548-
}
549544
}
550545

551546
/**
@@ -601,11 +596,11 @@ function create_item(anchor, prev, value, key, index, render_fn, flags, get_coll
601596

602597
item.e = branch(() => render_fn(/** @type {Node} */ (anchor), v, i, get_collection));
603598

604-
item.e.prev = prev && prev.e;
605-
606599
if (prev !== null) {
600+
// we only need to set `prev.next = item`, because
601+
// `item.prev = prev` was set on initialization.
602+
// the effects themselves are already linked
607603
prev.next = item;
608-
prev.e.next = item.e;
609604
}
610605

611606
return item;
@@ -640,12 +635,23 @@ function move(item, next, anchor) {
640635
function link(state, prev, next) {
641636
if (prev === null) {
642637
state.first = next;
638+
state.effect.first = next && next.e;
643639
} else {
640+
if (prev.e.next) {
641+
prev.e.next.prev = null;
642+
}
643+
644644
prev.next = next;
645645
prev.e.next = next && next.e;
646646
}
647647

648-
if (next !== null) {
648+
if (next === null) {
649+
state.effect.last = prev && prev.e;
650+
} else {
651+
if (next.e.prev) {
652+
next.e.prev.next = null;
653+
}
654+
649655
next.prev = prev;
650656
next.e.prev = prev && prev.e;
651657
}

packages/svelte/src/internal/client/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export type TemplateNode = Text | Element | Comment;
6464
export type Dom = TemplateNode | TemplateNode[];
6565

6666
export type EachState = {
67+
/** the each block effect */
68+
effect: Effect;
6769
/** flags */
6870
flags: number;
6971
/** a key -> item lookup */
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [step_back, step_forward, jump_back, jump_forward] = target.querySelectorAll('button');
7+
const [div] = target.querySelectorAll('div');
8+
9+
step_back.click();
10+
await tick();
11+
12+
step_forward.click();
13+
await tick();
14+
15+
step_forward.click();
16+
await tick();
17+
18+
// if the effects get linked in a circle, we will never get here
19+
assert.htmlEqual(div.innerHTML, '<p>5</p><p>6</p><p>7</p>');
20+
21+
jump_forward.click();
22+
await tick();
23+
24+
step_forward.click();
25+
await tick();
26+
27+
step_forward.click();
28+
await tick();
29+
30+
assert.htmlEqual(div.innerHTML, '<p>12</p><p>13</p><p>14</p>');
31+
}
32+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script lang="ts">
2+
let items = $state([4, 5, 6]);
3+
</script>
4+
5+
<button onclick={() => {
6+
items = items.map((n) => n - 1);
7+
}}>
8+
step back
9+
</button>
10+
11+
<button onclick={() => {
12+
items = items.map((n) => n + 1);
13+
}}>
14+
step forward
15+
</button>
16+
17+
<button onclick={() => {
18+
items = items.map((n) => n - 5);
19+
}}>
20+
jump back
21+
</button>
22+
23+
<button onclick={() => {
24+
items = items.map((n) => n + 5);
25+
}}>
26+
jump forward
27+
</button>
28+
29+
<div>
30+
{#each items as item (item)}
31+
<p>{item}</p>
32+
{/each}
33+
</div>

0 commit comments

Comments
 (0)