Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clever-months-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: only abort effect flushing if it causes an existing effect to be scheduled
24 changes: 10 additions & 14 deletions packages/svelte/src/internal/client/reactivity/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import * as e from '../errors.js';
import { flush_tasks } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import { old_values } from './sources.js';
import { old_values, schedule_version } from './sources.js';
import { unlink_effect } from './effects.js';
import { unset_context } from './async.js';

Expand Down Expand Up @@ -292,12 +292,12 @@ export class Batch {
if (!skip && effect.fn !== null) {
if (is_branch) {
effect.f ^= CLEAN;
} else if ((flags & EFFECT) !== 0) {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & CLEAN) === 0) {
if ((flags & EFFECT) !== 0) {
this.#effects.push(effect);
} else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) {
this.#render_effects.push(effect);
} else if ((flags & ASYNC) !== 0) {
if ((flags & ASYNC) !== 0) {
var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects;
effects.push(effect);
} else if (is_dirty(effect)) {
Expand Down Expand Up @@ -598,7 +598,7 @@ function flush_queued_effects(effects) {
var effect = effects[i++];

if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) {
var n = current_batch ? current_batch.current.size : 0;
var sv = schedule_version;

update_effect(effect);

Expand All @@ -619,13 +619,9 @@ function flush_queued_effects(effects) {
}
}

// 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 (
current_batch !== null &&
current_batch.current.size > n &&
(effect.f & USER_EFFECT) !== 0
) {
// if a state change in a user effect invalidates a _different_ effect,
// abort and reschedule in case that effect now needs to be destroyed
if (schedule_version > sv && (effect.f & USER_EFFECT) !== 0) {
break;
}
}
Expand Down
7 changes: 7 additions & 0 deletions packages/svelte/src/internal/client/reactivity/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@ export function increment(source) {
set(source, source.v + 1);
}

/**
* We increment this value when a block effect is scheduled as a result of a state change,
* as its currently-scheduled child effects may need to be destroyed
*/
export let schedule_version = 0;

/**
* @param {Value} signal
* @param {number} status should be DIRTY or MAYBE_DIRTY
Expand Down Expand Up @@ -334,6 +340,7 @@ function mark_reactions(signal, status) {
if ((flags & DERIVED) !== 0) {
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
} else if (not_dirty) {
if ((flags & BLOCK_EFFECT) !== 0) schedule_version += 1;
schedule_effect(/** @type {Effect} */ (reaction));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
let { children } = $props();

let inited = $state(false);

$effect(() => {
inited = true;
});
</script>

{#if inited}
<span>{@render children()}</span>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
async test({ assert, target }) {
const [button] = target.querySelectorAll('button');

assert.doesNotThrow(() => {
flushSync(() => button.click());
});
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
import Child from './Child.svelte';

let show = $state(false);
</script>

<button onclick={() => show = !show}>
toggle
</button>

{#if show}
{#each { length: 1234 } as i}
<Child>{i}</Child>
{/each}
{/if}
Loading