Skip to content

Commit 3999fed

Browse files
authored
fix: ensure each block inert items are disposed of if the each block is also inert (#13930)
Fixes #13926
1 parent e47ee22 commit 3999fed

File tree

5 files changed

+78
-9
lines changed

5 files changed

+78
-9
lines changed

.changeset/curly-dogs-breathe.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: ensure each block inert items are disposed of if the each block is also inert

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { source, mutable_source, internal_set } from '../../reactivity/sources.j
3535
import { array_from, is_array } from '../../../shared/utils.js';
3636
import { INERT } from '../../constants.js';
3737
import { queue_micro_task } from '../task.js';
38-
import { active_effect } from '../../runtime.js';
38+
import { active_effect, active_reaction } from '../../runtime.js';
3939

4040
/**
4141
* The row of a keyed each block that is currently updating. We track this
@@ -204,7 +204,8 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
204204
}
205205

206206
if (!hydrating) {
207-
reconcile(array, state, anchor, render_fn, flags, get_key);
207+
var effect = /** @type {Effect} */ (active_reaction);
208+
reconcile(array, state, anchor, render_fn, flags, (effect.f & INERT) !== 0, get_key);
208209
}
209210

210211
if (fallback_fn !== null) {
@@ -248,10 +249,11 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
248249
* @param {Element | Comment | Text} anchor
249250
* @param {(anchor: Node, item: MaybeSource<V>, index: number | Source<number>) => void} render_fn
250251
* @param {number} flags
252+
* @param {boolean} is_inert
251253
* @param {(value: V, index: number) => any} get_key
252254
* @returns {void}
253255
*/
254-
function reconcile(array, state, anchor, render_fn, flags, get_key) {
256+
function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key) {
255257
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
256258
var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
257259

@@ -390,9 +392,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
390392
stashed = [];
391393

392394
while (current !== null && current.k !== key) {
393-
// If the item has an effect that is already inert, skip over adding it
394-
// to our seen Set as the item is already being handled
395-
if ((current.e.f & INERT) === 0) {
395+
// If the each block isn't inert and an item has an effect that is already inert,
396+
// skip over adding it to our seen Set as the item is already being handled
397+
if (is_inert || (current.e.f & INERT) === 0) {
396398
(seen ??= new Set()).add(current);
397399
}
398400
stashed.push(current);
@@ -415,8 +417,8 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
415417
var to_destroy = seen === undefined ? [] : array_from(seen);
416418

417419
while (current !== null) {
418-
// Inert effects are currently outroing and will be removed once the transition is finished
419-
if ((current.e.f & INERT) === 0) {
420+
// If the each block isn't inert, then inert effects are currently outroing and will be removed once the transition is finished
421+
if (is_inert || (current.e.f & INERT) === 0) {
420422
to_destroy.push(current);
421423
}
422424
current = current.next;

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,14 +577,17 @@ export function resume_effect(effect) {
577577
*/
578578
function resume_children(effect, local) {
579579
if ((effect.f & INERT) === 0) return;
580-
effect.f ^= INERT;
581580

582581
// If a dependency of this effect changed while it was paused,
583582
// apply the change now
584583
if (check_dirtiness(effect)) {
585584
update_effect(effect);
586585
}
587586

587+
// Ensure we toggle the flag after possibly updating the effect so that
588+
// each block logic can correctly operate on inert items
589+
effect.f ^= INERT;
590+
588591
var child = effect.first;
589592

590593
while (child !== null) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { flushSync } from '../../../../src/index-client.js';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, raf, target }) {
6+
assert.htmlEqual(
7+
target.innerHTML,
8+
'<button>Toggle</button><div><div>1</div><div>2</div><div>3</div></div>'
9+
);
10+
11+
const btn1 = target.querySelector('button');
12+
btn1?.click();
13+
flushSync();
14+
raf.tick(250);
15+
16+
assert.htmlEqual(
17+
target.innerHTML,
18+
'<button>Toggle</button><div style="opacity: 0.5;"><div>1</div><div>2</div><div>3</div></div>'
19+
);
20+
21+
await Promise.resolve();
22+
23+
flushSync();
24+
raf.tick(500);
25+
26+
assert.htmlEqual(
27+
target.innerHTML,
28+
'<button>Toggle</button><div style=""><div>3</div><div>4</div></div>'
29+
);
30+
}
31+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script>
2+
function fade(_) {
3+
return {
4+
duration: 500,
5+
css: t => `opacity: ${t}`,
6+
}
7+
}
8+
9+
let toggle = $state(true);
10+
let items = $state([ 1, 2, 3 ]);
11+
12+
const handle_toggle = async () => {
13+
toggle = false;
14+
await Promise.resolve();
15+
items = [3, 4];
16+
toggle = true;
17+
};
18+
</script>
19+
20+
<button onclick={handle_toggle}>Toggle</button>
21+
22+
{#if toggle}
23+
<div transition:fade>
24+
{#each items as item (item)}
25+
<div>{item}</div>
26+
{/each}
27+
</div>
28+
{/if}

0 commit comments

Comments
 (0)