Skip to content

Commit 72185af

Browse files
committed
fix: change title only after any pending work has completed
We have to use an effect - not a render effect - for updating the title, and always. That way we change the title only after any pending work has completed. Fixes #17060
1 parent 83746ad commit 72185af

File tree

10 files changed

+128
-8
lines changed

10 files changed

+128
-8
lines changed

.changeset/legal-mangos-peel.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: change title only after any pending work has completed

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
/** @import { AST } from '#compiler' */
22
/** @import { ComponentContext } from '../types' */
33
import * as b from '#compiler/builders';
4-
import { build_template_chunk } from './shared/utils.js';
4+
import { build_template_chunk, Memoizer } from './shared/utils.js';
55

66
/**
77
* @param {AST.TitleElement} node
88
* @param {ComponentContext} context
99
*/
1010
export function TitleElement(node, context) {
11+
const memoizer = new Memoizer();
1112
const { has_state, value } = build_template_chunk(
1213
/** @type {any} */ (node.fragment.nodes),
13-
context
14+
context,
15+
context.state,
16+
(value, metadata) => memoizer.add(value, metadata)
1417
);
1518
const evaluated = context.state.scope.evaluate(value);
1619

@@ -26,9 +29,21 @@ export function TitleElement(node, context) {
2629
)
2730
);
2831

32+
// Always in an $effect so it only changes the title once async work is done
2933
if (has_state) {
30-
context.state.update.push(statement);
34+
context.state.after_update.push(
35+
b.stmt(
36+
b.call(
37+
'$.template_effect',
38+
b.arrow(memoizer.apply(), b.block([statement])),
39+
memoizer.sync_values(),
40+
memoizer.async_values(),
41+
memoizer.blockers(),
42+
b.true
43+
)
44+
)
45+
);
3146
} else {
32-
context.state.init.push(statement);
47+
context.state.after_update.push(b.stmt(b.call('$.effect', b.thunk(b.block([statement])))));
3348
}
3449
}

packages/svelte/src/internal/client/dom/blocks/svelte-head.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { TemplateNode } from '#client' */
22
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
33
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
4-
import { block } from '../../reactivity/effects.js';
4+
import { block, branch } from '../../reactivity/effects.js';
55
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
66

77
/**
@@ -49,7 +49,7 @@ export function head(hash, render_fn) {
4949
}
5050

5151
try {
52-
block(() => render_fn(anchor), HEAD_EFFECT);
52+
block(() => branch(() => render_fn(anchor)), HEAD_EFFECT);
5353
} finally {
5454
if (was_hydrating) {
5555
set_hydrating(true);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,10 +366,11 @@ 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
369370
*/
370-
export function template_effect(fn, sync = [], async = [], blockers = []) {
371+
export function template_effect(fn, sync = [], async = [], blockers = [], defer = false) {
371372
flatten(blockers, sync, async, (values) => {
372-
create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true);
373+
create_effect(defer ? EFFECT : RENDER_EFFECT, () => fn(...values.map(get)), true);
373374
});
374375
}
375376

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
let { deferred } = $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} />
12+
{/if}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let { deferred } = $props();
3+
4+
function push() {
5+
const d = Promise.withResolvers();
6+
deferred.push(() => d.resolve('title'));
7+
return d.promise;
8+
}
9+
</script>
10+
11+
<svelte:head>
12+
<title>{await push()}</title>
13+
</svelte:head>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
assert.equal(window.document.title, 'title');
22+
}
23+
});
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} />
12+
{/if}

0 commit comments

Comments
 (0)