diff --git a/.changeset/cold-beds-look.md b/.changeset/cold-beds-look.md new file mode 100644 index 0000000000..0929e42a89 --- /dev/null +++ b/.changeset/cold-beds-look.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly defer document title until async work is complete diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index edd8835e00..f815f3ae05 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -29,17 +29,16 @@ export function TitleElement(node, context) { ) ); - // Always in an $effect so it only changes the title once async work is done + // Make sure it only changes the title once async work is done if (has_state) { context.state.after_update.push( b.stmt( b.call( - '$.template_effect', + '$.deferred_template_effect', b.arrow(memoizer.apply(), b.block([statement])), memoizer.sync_values(), memoizer.async_values(), - memoizer.blockers(), - b.true + memoizer.blockers() ) ) ); diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 3807c63c20..a2add3ec59 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -119,6 +119,7 @@ export { legacy_pre_effect_reset, render_effect, template_effect, + deferred_template_effect, effect, user_effect, user_pre_effect diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 84308ef3ed..ab5bd0b788 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -144,6 +144,10 @@ export class Batch { is_fork = false; + is_deferred() { + return this.is_fork || this.#blocking_pending > 0; + } + /** * * @param {Effect[]} root_effects @@ -172,7 +176,7 @@ export class Batch { this.#resolve(); } - if (this.#blocking_pending > 0 || this.is_fork) { + if (this.is_deferred()) { this.#defer_effects(target.effects); this.#defer_effects(target.render_effects); this.#defer_effects(target.block_effects); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 5d7c0ef871..658e23842f 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -40,7 +40,7 @@ import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { component_context, dev_current_component_function, dev_stack } from '../context.js'; -import { Batch, schedule_effect } from './batch.js'; +import { Batch, current_batch, schedule_effect } from './batch.js'; import { flatten } from './async.js'; import { without_reactive_context } from '../dom/elements/bindings/shared.js'; @@ -366,11 +366,29 @@ export function render_effect(fn, flags = 0) { * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {Array>} blockers - * @param {boolean} defer */ -export function template_effect(fn, sync = [], async = [], blockers = [], defer = false) { +export function template_effect(fn, sync = [], async = [], blockers = []) { flatten(blockers, sync, async, (values) => { - create_effect(defer ? EFFECT : RENDER_EFFECT, () => fn(...values.map(get)), true); + create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true); + }); +} + +/** + * Like `template_effect`, but with an effect which is deferred until the batch commits + * @param {(...expressions: any) => void | (() => void)} fn + * @param {Array<() => any>} sync + * @param {Array<() => Promise>} async + * @param {Array>} blockers + */ +export function deferred_template_effect(fn, sync = [], async = [], blockers = []) { + var batch = /** @type {Batch} */ (current_batch); + var is_async = async.length > 0 || blockers.length > 0; + + if (is_async) batch.increment(true); + + flatten(blockers, sync, async, (values) => { + create_effect(EFFECT, () => fn(...values.map(get)), false); + if (is_async) batch.decrement(true); }); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-3/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/Inner.svelte new file mode 100644 index 0000000000..4d761c92ef --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/Inner.svelte @@ -0,0 +1,15 @@ + + + + {title} + + +

{await push()}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/_config.js new file mode 100644 index 0000000000..39cbf5becb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/main.svelte new file mode 100644 index 0000000000..be4f04afe8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-3/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if}