fix: properly defer document title until async work is complete (#17158)

#17061 didn't properly handle the case where the title is sync but reactive and async work outside is pending. Handle this by creating a proper effect for the document title, and make sure to wait on it and flush it once ready.

Fixes #17114
pull/17159/head
Simon H 1 day ago committed by GitHub
parent e238e6611e
commit a0c7c3b327
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly defer document title until async work is complete

@ -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()
)
)
);

@ -119,6 +119,7 @@ export {
legacy_pre_effect_reset,
render_effect,
template_effect,
deferred_template_effect,
effect,
user_effect,
user_pre_effect

@ -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);

@ -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<any>>} async
* @param {Array<Promise<void>>} 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<any>>} async
* @param {Array<Promise<void>>} 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);
});
}

@ -0,0 +1,15 @@
<script>
let { deferred, title } = $props();
function push() {
const d = Promise.withResolvers();
deferred.push(() => d.resolve());
return d.promise;
}
</script>
<svelte:head>
<title>{title}</title>
</svelte:head>
<p>{await push()}</p>

@ -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');
}
});

@ -0,0 +1,12 @@
<script>
import Inner from './Inner.svelte';
let deferred = [];
let show = $state(false);
</script>
<button onclick={() => show = !show}>toggle</button>
<button onclick={() => deferred.pop()()}>resolve</button>
{#if show}
<Inner {deferred} title="title" />
{/if}
Loading…
Cancel
Save