Skip to content

Commit 7a153ce

Browse files
committed
fix: skip effects inside dynamic component that is about to be destroyed
When a dynamic component was updated to a different instance and its props were updated at the same time, effects inside the component were still called with the already-changed props. The fix is to mark the branch as skipped to never got to those effects. Fixes #16387
1 parent d2ba258 commit 7a153ce

File tree

7 files changed

+47
-2
lines changed

7 files changed

+47
-2
lines changed

.changeset/neat-files-do.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: skip effects inside dynamic component that is about to be destroyed

packages/svelte/src/internal/client/dom/blocks/svelte-component.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ export function component(node, get_component, render_fn) {
6262
if (defer) {
6363
offscreen_fragment = document.createDocumentFragment();
6464
offscreen_fragment.append((target = create_text()));
65+
if (effect) {
66+
/** @type {Batch} */ (current_batch).skipped_effects.add(effect);
67+
}
6568
}
66-
6769
pending_effect = branch(() => render_fn(target, component));
6870
}
6971

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
RENDER_EFFECT,
1212
ROOT_EFFECT,
1313
USER_EFFECT,
14-
MAYBE_DIRTY
14+
MAYBE_DIRTY,
15+
EFFECT_RAN
1516
} from '#client/constants';
1617
import { async_mode_flag } from '../../flags/index.js';
1718
import { deferred, define_property } from '../../shared/utils.js';
@@ -599,6 +600,7 @@ function flush_queued_effects(effects) {
599600

600601
if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
601602
var n = current_batch ? current_batch.current.size : 0;
603+
var ran = effect.f & EFFECT_RAN;
602604

603605
update_effect(effect);
604606

@@ -622,6 +624,7 @@ function flush_queued_effects(effects) {
622624
// if state is written in a user effect, abort and re-schedule, lest we run
623625
// effects that should be removed as a result of the state change
624626
if (
627+
// ran &&
625628
current_batch !== null &&
626629
current_batch.current.size > n &&
627630
(effect.f & USER_EFFECT) !== 0
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let { data } = $props();
3+
</script>
4+
5+
{#each data.obj.arr as i}
6+
<p>{i}</p>
7+
{/each}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>Comp 2</p>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [btn] = target.querySelectorAll('button');
7+
btn.click();
8+
flushSync();
9+
assert.htmlEqual(target.innerHTML, `<button>Change</button> <p>Comp 2</p>`);
10+
}
11+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
import Comp_1 from './Comp-1.svelte';
3+
import Comp_2 from './Comp-2.svelte';
4+
5+
let Comp = $state.raw(Comp_1);
6+
let data = $state.raw({ obj: { arr: [1, 2, 3] } });
7+
8+
function change() {
9+
Comp = Comp_2;
10+
data = {};
11+
}
12+
</script>
13+
14+
<button onclick={change}>Change</button>
15+
16+
<Comp {data} />

0 commit comments

Comments
 (0)