Skip to content

Commit 0e709e3

Browse files
authored
fix: change title only after any pending work has completed (#17061)
* 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 * fix
1 parent 723c421 commit 0e709e3

File tree

10 files changed

+135
-18
lines changed

10 files changed

+135
-18
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/reactivity/batch.js

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,25 @@ import {
1414
MAYBE_DIRTY,
1515
DERIVED,
1616
BOUNDARY_EFFECT,
17-
EAGER_EFFECT
17+
EAGER_EFFECT,
18+
HEAD_EFFECT
1819
} from '#client/constants';
1920
import { async_mode_flag } from '../../flags/index.js';
2021
import { deferred, define_property } from '../../shared/utils.js';
2122
import {
2223
active_effect,
2324
get,
24-
increment_write_version,
2525
is_dirty,
2626
is_updating_effect,
2727
set_is_updating_effect,
2828
set_signal_status,
29-
tick,
3029
update_effect
3130
} from '../runtime.js';
3231
import * as e from '../errors.js';
3332
import { flush_tasks, queue_micro_task } from '../dom/task.js';
3433
import { DEV } from 'esm-env';
3534
import { invoke_error_boundary } from '../error-handling.js';
36-
import {
37-
flush_eager_effects,
38-
eager_effects,
39-
old_values,
40-
set_eager_effects,
41-
source,
42-
update
43-
} from './sources.js';
35+
import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
4436
import { eager_effect, unlink_effect } from './effects.js';
4537

4638
/**
@@ -800,7 +792,12 @@ export function schedule_effect(signal) {
800792

801793
// if the effect is being scheduled because a parent (each/await/etc) block
802794
// updated an internal source, bail out or we'll cause a second flush
803-
if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) {
795+
if (
796+
is_flushing &&
797+
effect === active_effect &&
798+
(flags & BLOCK_EFFECT) !== 0 &&
799+
(flags & HEAD_EFFECT) === 0
800+
) {
804801
return;
805802
}
806803

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)