fix: abort and reschedule effect processing after state change in user effect

pull/16280/head
Rich Harris 3 months ago
parent 2f68131e9a
commit dccc31d6ec

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: abort and reschedule effect processing after state change in user effect

@ -19,6 +19,7 @@ export const INSPECT_EFFECT = 1 << 18;
export const HEAD_EFFECT = 1 << 19; export const HEAD_EFFECT = 1 << 19;
export const EFFECT_HAS_DERIVED = 1 << 20; export const EFFECT_HAS_DERIVED = 1 << 20;
export const EFFECT_IS_UPDATING = 1 << 21; export const EFFECT_IS_UPDATING = 1 << 21;
export const USER_EFFECT = 1 << 22;
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props'); export const LEGACY_PROPS = Symbol('legacy props');

@ -9,7 +9,7 @@ import {
set_active_effect, set_active_effect,
set_active_reaction set_active_reaction
} from './runtime.js'; } from './runtime.js';
import { effect, teardown } from './reactivity/effects.js'; import { create_user_effect, teardown } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js'; import { legacy_mode_flag } from '../flags/index.js';
/** @type {ComponentContext | null} */ /** @type {ComponentContext | null} */
@ -153,7 +153,7 @@ export function pop(component) {
var component_effect = component_effects[i]; var component_effect = component_effects[i];
set_active_effect(component_effect.effect); set_active_effect(component_effect.effect);
set_active_reaction(component_effect.reaction); set_active_reaction(component_effect.reaction);
effect(component_effect.fn); create_user_effect(component_effect.fn);
} }
} finally { } finally {
set_active_effect(previous_effect); set_active_effect(previous_effect);

@ -33,7 +33,8 @@ import {
MAYBE_DIRTY, MAYBE_DIRTY,
EFFECT_HAS_DERIVED, EFFECT_HAS_DERIVED,
BOUNDARY_EFFECT, BOUNDARY_EFFECT,
STALE_REACTION STALE_REACTION,
USER_EFFECT
} from '#client/constants'; } from '#client/constants';
import { set } from './sources.js'; import { set } from './sources.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
@ -199,11 +200,17 @@ export function user_effect(fn) {
reaction: active_reaction reaction: active_reaction
}); });
} else { } else {
var signal = effect(fn); return create_user_effect(fn);
return signal;
} }
} }
/**
* @param {() => void | (() => void)} fn
*/
export function create_user_effect(fn) {
return create_effect(EFFECT | USER_EFFECT, fn, false);
}
/** /**
* Internal representation of `$effect.pre(...)` * Internal representation of `$effect.pre(...)`
* @param {() => void | (() => void)} fn * @param {() => void | (() => void)} fn
@ -216,7 +223,7 @@ export function user_pre_effect(fn) {
value: '$effect.pre' value: '$effect.pre'
}); });
} }
return render_effect(fn); return create_effect(RENDER_EFFECT | USER_EFFECT, fn, true);
} }
/** @param {() => void | (() => void)} fn */ /** @param {() => void | (() => void)} fn */

@ -22,7 +22,8 @@ import {
ROOT_EFFECT, ROOT_EFFECT,
DISCONNECTED, DISCONNECTED,
EFFECT_IS_UPDATING, EFFECT_IS_UPDATING,
STALE_REACTION STALE_REACTION,
USER_EFFECT
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { internal_set, old_values } from './reactivity/sources.js'; import { internal_set, old_values } from './reactivity/sources.js';
@ -571,6 +572,8 @@ function flush_queued_effects(effects) {
if ((effect.f & (DESTROYED | INERT)) === 0) { if ((effect.f & (DESTROYED | INERT)) === 0) {
if (check_dirtiness(effect)) { if (check_dirtiness(effect)) {
var wv = write_version;
update_effect(effect); update_effect(effect);
// Effects with no dependencies or teardown do not get added to the effect tree. // Effects with no dependencies or teardown do not get added to the effect tree.
@ -587,9 +590,19 @@ function flush_queued_effects(effects) {
effect.fn = null; effect.fn = null;
} }
} }
// if state is written in a user effect, abort and re-schedule, lest we run
// effects that should be removed as a result of the state change
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
break;
}
} }
} }
} }
for (; i < length; i += 1) {
schedule_effect(effects[i]);
}
} }
/** /**

@ -0,0 +1,11 @@
<script>
import B from './B.svelte';
let { boolean, closed } = $props();
$effect(() => {
console.log(boolean);
});
</script>
<B {closed} />

@ -0,0 +1,9 @@
<script>
import { close } from './Child.svelte';
let { closed } = $props();
$effect(() => {
if (closed) close();
});
</script>

@ -0,0 +1,20 @@
<script module>
let object = $state();
export function open() {
object = { boolean: true };
}
export function close() {
object = undefined;
}
</script>
<script>
let { children } = $props();
</script>
{#if object?.boolean}
<!-- error occurs here, this is executed when the if should already make it falsy -->
{@render children(object.boolean)}
{/if}

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [open, close] = target.querySelectorAll('button');
flushSync(() => open.click());
flushSync(() => close.click());
assert.deepEqual(logs, [true]);
}
});

@ -0,0 +1,23 @@
<script>
import A from './A.svelte';
import Child, { open } from './Child.svelte';
let closed = $state(false);
</script>
<button onclick={open}>
open
</button>
<button onclick={() => closed = true}>
close
</button>
<hr>
<Child>
{#snippet children(boolean)}
<A {closed} {boolean} />
{/snippet}
</Child>
Loading…
Cancel
Save