Skip to content

Commit dccc31d

Browse files
committed
fix: abort and reschedule effect processing after state change in user effect
1 parent 2f68131 commit dccc31d

File tree

10 files changed

+109
-7
lines changed

10 files changed

+109
-7
lines changed

.changeset/cuddly-walls-tan.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: abort and reschedule effect processing after state change in user effect

packages/svelte/src/internal/client/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const INSPECT_EFFECT = 1 << 18;
1919
export const HEAD_EFFECT = 1 << 19;
2020
export const EFFECT_HAS_DERIVED = 1 << 20;
2121
export const EFFECT_IS_UPDATING = 1 << 21;
22+
export const USER_EFFECT = 1 << 22;
2223

2324
export const STATE_SYMBOL = Symbol('$state');
2425
export const LEGACY_PROPS = Symbol('legacy props');

packages/svelte/src/internal/client/context.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
set_active_effect,
1010
set_active_reaction
1111
} from './runtime.js';
12-
import { effect, teardown } from './reactivity/effects.js';
12+
import { create_user_effect, teardown } from './reactivity/effects.js';
1313
import { legacy_mode_flag } from '../flags/index.js';
1414

1515
/** @type {ComponentContext | null} */
@@ -153,7 +153,7 @@ export function pop(component) {
153153
var component_effect = component_effects[i];
154154
set_active_effect(component_effect.effect);
155155
set_active_reaction(component_effect.reaction);
156-
effect(component_effect.fn);
156+
create_user_effect(component_effect.fn);
157157
}
158158
} finally {
159159
set_active_effect(previous_effect);

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ import {
3333
MAYBE_DIRTY,
3434
EFFECT_HAS_DERIVED,
3535
BOUNDARY_EFFECT,
36-
STALE_REACTION
36+
STALE_REACTION,
37+
USER_EFFECT
3738
} from '#client/constants';
3839
import { set } from './sources.js';
3940
import * as e from '../errors.js';
@@ -199,11 +200,17 @@ export function user_effect(fn) {
199200
reaction: active_reaction
200201
});
201202
} else {
202-
var signal = effect(fn);
203-
return signal;
203+
return create_user_effect(fn);
204204
}
205205
}
206206

207+
/**
208+
* @param {() => void | (() => void)} fn
209+
*/
210+
export function create_user_effect(fn) {
211+
return create_effect(EFFECT | USER_EFFECT, fn, false);
212+
}
213+
207214
/**
208215
* Internal representation of `$effect.pre(...)`
209216
* @param {() => void | (() => void)} fn
@@ -216,7 +223,7 @@ export function user_pre_effect(fn) {
216223
value: '$effect.pre'
217224
});
218225
}
219-
return render_effect(fn);
226+
return create_effect(RENDER_EFFECT | USER_EFFECT, fn, true);
220227
}
221228

222229
/** @param {() => void | (() => void)} fn */

packages/svelte/src/internal/client/runtime.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
ROOT_EFFECT,
2323
DISCONNECTED,
2424
EFFECT_IS_UPDATING,
25-
STALE_REACTION
25+
STALE_REACTION,
26+
USER_EFFECT
2627
} from './constants.js';
2728
import { flush_tasks } from './dom/task.js';
2829
import { internal_set, old_values } from './reactivity/sources.js';
@@ -571,6 +572,8 @@ function flush_queued_effects(effects) {
571572

572573
if ((effect.f & (DESTROYED | INERT)) === 0) {
573574
if (check_dirtiness(effect)) {
575+
var wv = write_version;
576+
574577
update_effect(effect);
575578

576579
// Effects with no dependencies or teardown do not get added to the effect tree.
@@ -587,9 +590,19 @@ function flush_queued_effects(effects) {
587590
effect.fn = null;
588591
}
589592
}
593+
594+
// if state is written in a user effect, abort and re-schedule, lest we run
595+
// effects that should be removed as a result of the state change
596+
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
597+
break;
598+
}
590599
}
591600
}
592601
}
602+
603+
for (; i < length; i += 1) {
604+
schedule_effect(effects[i]);
605+
}
593606
}
594607

595608
/**
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
import B from './B.svelte';
3+
4+
let { boolean, closed } = $props();
5+
6+
$effect(() => {
7+
console.log(boolean);
8+
});
9+
</script>
10+
11+
<B {closed} />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
import { close } from './Child.svelte';
3+
4+
let { closed } = $props();
5+
6+
$effect(() => {
7+
if (closed) close();
8+
});
9+
</script>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script module>
2+
let object = $state();
3+
4+
export function open() {
5+
object = { boolean: true };
6+
}
7+
8+
export function close() {
9+
object = undefined;
10+
}
11+
</script>
12+
13+
<script>
14+
let { children } = $props();
15+
</script>
16+
17+
{#if object?.boolean}
18+
<!-- error occurs here, this is executed when the if should already make it falsy -->
19+
{@render children(object.boolean)}
20+
{/if}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target, logs }) {
6+
const [open, close] = target.querySelectorAll('button');
7+
8+
flushSync(() => open.click());
9+
flushSync(() => close.click());
10+
11+
assert.deepEqual(logs, [true]);
12+
}
13+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script>
2+
import A from './A.svelte';
3+
import Child, { open } from './Child.svelte';
4+
5+
let closed = $state(false);
6+
</script>
7+
8+
<button onclick={open}>
9+
open
10+
</button>
11+
12+
<button onclick={() => closed = true}>
13+
close
14+
</button>
15+
16+
<hr>
17+
18+
<Child>
19+
{#snippet children(boolean)}
20+
<A {closed} {boolean} />
21+
{/snippet}
22+
</Child>
23+

0 commit comments

Comments
 (0)