Skip to content

Commit cf2ff5e

Browse files
authored
fix: abort and reschedule effect processing after state change in user effect (#16280)
* fix: abort and reschedule effect processing after state change in user effect * failing test * skip for now
1 parent 63e4836 commit cf2ff5e

File tree

15 files changed

+184
-7
lines changed

15 files changed

+184
-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 << 17;
1919
export const HEAD_EFFECT = 1 << 18;
2020
export const EFFECT_PRESERVED = 1 << 19;
2121
export const EFFECT_IS_UPDATING = 1 << 20;
22+
export const USER_EFFECT = 1 << 21;
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
import { FILENAME } from '../../constants.js';
1515

@@ -191,7 +191,7 @@ export function pop(component) {
191191
var component_effect = component_effects[i];
192192
set_active_effect(component_effect.effect);
193193
set_active_reaction(component_effect.reaction);
194-
effect(component_effect.fn);
194+
create_user_effect(component_effect.fn);
195195
}
196196
} finally {
197197
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_PRESERVED,
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';
@@ -200,11 +201,17 @@ export function user_effect(fn) {
200201
reaction: active_reaction
201202
});
202203
} else {
203-
var signal = effect(fn);
204-
return signal;
204+
return create_user_effect(fn);
205205
}
206206
}
207207

208+
/**
209+
* @param {() => void | (() => void)} fn
210+
*/
211+
export function create_user_effect(fn) {
212+
return create_effect(EFFECT | USER_EFFECT, fn, false);
213+
}
214+
208215
/**
209216
* Internal representation of `$effect.pre(...)`
210217
* @param {() => void | (() => void)} fn
@@ -217,7 +224,7 @@ export function user_pre_effect(fn) {
217224
value: '$effect.pre'
218225
});
219226
}
220-
return render_effect(fn);
227+
return create_effect(RENDER_EFFECT | USER_EFFECT, fn, true);
221228
}
222229

223230
/** @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';
@@ -581,6 +582,8 @@ function flush_queued_effects(effects) {
581582

582583
if ((effect.f & (DESTROYED | INERT)) === 0) {
583584
if (check_dirtiness(effect)) {
585+
var wv = write_version;
586+
584587
update_effect(effect);
585588

586589
// Effects with no dependencies or teardown do not get added to the effect tree.
@@ -597,9 +600,19 @@ function flush_queued_effects(effects) {
597600
effect.fn = null;
598601
}
599602
}
603+
604+
// if state is written in a user effect, abort and re-schedule, lest we run
605+
// effects that should be removed as a result of the state change
606+
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
607+
break;
608+
}
600609
}
601610
}
602611
}
612+
613+
for (; i < length; i += 1) {
614+
schedule_effect(effects[i]);
615+
}
603616
}
604617

605618
/**
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)