Skip to content

Commit 8549eef

Browse files
committed
fix: properly defer document title until async work is complete
#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
1 parent e238e66 commit 8549eef

File tree

8 files changed

+87
-9
lines changed

8 files changed

+87
-9
lines changed

.changeset/cold-beds-look.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: properly defer document title until async work is complete

packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,16 @@ export function TitleElement(node, context) {
2929
)
3030
);
3131

32-
// Always in an $effect so it only changes the title once async work is done
32+
// Make sure it only changes the title once async work is done
3333
if (has_state) {
3434
context.state.after_update.push(
3535
b.stmt(
3636
b.call(
37-
'$.template_effect',
37+
'$.deferred_template_effect',
3838
b.arrow(memoizer.apply(), b.block([statement])),
3939
memoizer.sync_values(),
4040
memoizer.async_values(),
41-
memoizer.blockers(),
42-
b.true
41+
memoizer.blockers()
4342
)
4443
)
4544
);

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export {
119119
legacy_pre_effect_reset,
120120
render_effect,
121121
template_effect,
122+
deferred_template_effect,
122123
effect,
123124
user_effect,
124125
user_pre_effect

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ export class Batch {
144144

145145
is_fork = false;
146146

147+
is_deferred() {
148+
return this.is_fork || this.#blocking_pending > 0;
149+
}
150+
147151
/**
148152
*
149153
* @param {Effect[]} root_effects
@@ -172,7 +176,7 @@ export class Batch {
172176
this.#resolve();
173177
}
174178

175-
if (this.#blocking_pending > 0 || this.is_fork) {
179+
if (this.is_deferred()) {
176180
this.#defer_effects(target.effects);
177181
this.#defer_effects(target.render_effects);
178182
this.#defer_effects(target.block_effects);

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { DEV } from 'esm-env';
4040
import { define_property } from '../../shared/utils.js';
4141
import { get_next_sibling } from '../dom/operations.js';
4242
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
43-
import { Batch, schedule_effect } from './batch.js';
43+
import { Batch, current_batch, schedule_effect } from './batch.js';
4444
import { flatten } from './async.js';
4545
import { without_reactive_context } from '../dom/elements/bindings/shared.js';
4646

@@ -366,11 +366,29 @@ export function render_effect(fn, flags = 0) {
366366
* @param {Array<() => any>} sync
367367
* @param {Array<() => Promise<any>>} async
368368
* @param {Array<Promise<void>>} blockers
369-
* @param {boolean} defer
370369
*/
371-
export function template_effect(fn, sync = [], async = [], blockers = [], defer = false) {
370+
export function template_effect(fn, sync = [], async = [], blockers = []) {
372371
flatten(blockers, sync, async, (values) => {
373-
create_effect(defer ? EFFECT : RENDER_EFFECT, () => fn(...values.map(get)), true);
372+
create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true);
373+
});
374+
}
375+
376+
/**
377+
* Like `template_effect`, but with an effect which is deferred until the batch commits
378+
* @param {(...expressions: any) => void | (() => void)} fn
379+
* @param {Array<() => any>} sync
380+
* @param {Array<() => Promise<any>>} async
381+
* @param {Array<Promise<void>>} blockers
382+
*/
383+
export function deferred_template_effect(fn, sync = [], async = [], blockers = []) {
384+
var batch = /** @type {Batch} */ (current_batch);
385+
var is_async = async.length > 0 || blockers.length > 0;
386+
387+
if (is_async) batch.increment(true);
388+
389+
flatten(blockers, sync, async, (values) => {
390+
create_effect(EFFECT, () => fn(...values.map(get)), false);
391+
if (is_async) batch.decrement(true);
374392
});
375393
}
376394

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
let { deferred, title } = $props();
3+
4+
function push() {
5+
const d = Promise.withResolvers();
6+
deferred.push(() => d.resolve());
7+
return d.promise;
8+
}
9+
</script>
10+
11+
<svelte:head>
12+
<title>{title}</title>
13+
</svelte:head>
14+
15+
<p>{await push()}</p>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const [toggle, resolve] = target.querySelectorAll('button');
7+
toggle.click();
8+
await tick();
9+
assert.equal(window.document.title, '');
10+
11+
toggle.click();
12+
await tick();
13+
assert.equal(window.document.title, '');
14+
15+
toggle.click();
16+
await tick();
17+
assert.equal(window.document.title, '');
18+
19+
resolve.click();
20+
await tick();
21+
await tick();
22+
assert.equal(window.document.title, 'title');
23+
}
24+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
import Inner from './Inner.svelte';
3+
4+
let deferred = [];
5+
let show = $state(false);
6+
</script>
7+
8+
<button onclick={() => show = !show}>toggle</button>
9+
<button onclick={() => deferred.pop()()}>resolve</button>
10+
{#if show}
11+
<Inner {deferred} title="title" />
12+
{/if}

0 commit comments

Comments
 (0)