Skip to content

Commit afa3128

Browse files
trueadmRich-Harris
andauthored
breaking: avoid flushing queued updates on mount/hydrate (#12602)
* Revert "Revert "breaking: avoid flushing queued updates on mount/hydrate" (#1…" This reverts commit 8d13921. * fix legacy wrapper * lint * docs * duplicate --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 75ea6da commit afa3128

File tree

10 files changed

+51
-34
lines changed

10 files changed

+51
-34
lines changed

.changeset/slow-gorillas-yawn.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+
breaking: avoid flushing queued updates on mount/hydrate

documentation/docs/04-runtime/04-imperative-component-api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const app = mount(App, {
2929

3030
You can mount multiple components per page, and you can also mount from within your application, for example when creating a tooltip component and attaching it to the hovered element.
3131

32+
Note that unlike calling `new App(...)` in Svelte 4, things like effects (including `onMount` callbacks, and action functions) will not run during `mount`. If you need to force pending effects to run (in the context of a test, for example) you can do so with `flushSync()`.
33+
3234
## `unmount`
3335

3436
Unmounts a component created with [`mount`](#mount) or [`hydrate`](#hydrate):
@@ -74,3 +76,5 @@ const app = hydrate(App, {
7476
props: { some: 'property' }
7577
});
7678
```
79+
80+
As with `mount`, effects will not run during `hydrate` — use `flushSync()` immediately afterwards if you need them to.

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

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ export function set_text(text, value) {
8181
*/
8282
export function mount(component, options) {
8383
const anchor = options.anchor ?? options.target.appendChild(empty());
84-
// Don't flush previous effects to ensure order of outer effects stays consistent
85-
return flush_sync(() => _mount(component, { ...options, anchor }), false);
84+
return _mount(component, { ...options, anchor });
8685
}
8786

8887
/**
@@ -115,38 +114,35 @@ export function hydrate(component, options) {
115114
const previous_hydrate_node = hydrate_node;
116115

117116
try {
118-
// Don't flush previous effects to ensure order of outer effects stays consistent
119-
return flush_sync(() => {
120-
var anchor = /** @type {TemplateNode} */ (target.firstChild);
121-
while (
122-
anchor &&
123-
(anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
124-
) {
125-
anchor = /** @type {TemplateNode} */ (anchor.nextSibling);
126-
}
117+
var anchor = /** @type {TemplateNode} */ (target.firstChild);
118+
while (
119+
anchor &&
120+
(anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
121+
) {
122+
anchor = /** @type {TemplateNode} */ (anchor.nextSibling);
123+
}
127124

128-
if (!anchor) {
129-
throw HYDRATION_ERROR;
130-
}
125+
if (!anchor) {
126+
throw HYDRATION_ERROR;
127+
}
131128

132-
set_hydrating(true);
133-
set_hydrate_node(/** @type {Comment} */ (anchor));
134-
hydrate_next();
129+
set_hydrating(true);
130+
set_hydrate_node(/** @type {Comment} */ (anchor));
131+
hydrate_next();
135132

136-
const instance = _mount(component, { ...options, anchor });
133+
const instance = _mount(component, { ...options, anchor });
137134

138-
if (
139-
hydrate_node.nodeType !== 8 ||
140-
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
141-
) {
142-
w.hydration_mismatch();
143-
throw HYDRATION_ERROR;
144-
}
135+
if (
136+
hydrate_node.nodeType !== 8 ||
137+
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
138+
) {
139+
w.hydration_mismatch();
140+
throw HYDRATION_ERROR;
141+
}
145142

146-
set_hydrating(false);
143+
set_hydrating(false);
147144

148-
return instance;
149-
}, false);
145+
return /** @type {Exports} */ (instance);
150146
} catch (error) {
151147
if (error === HYDRATION_ERROR) {
152148
// TODO it's possible for event listeners to have been added and

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -671,10 +671,9 @@ function process_effects(effect, collected_effects) {
671671
* Internal version of `flushSync` with the option to not flush previous effects.
672672
* Returns the result of the passed function, if given.
673673
* @param {() => any} [fn]
674-
* @param {boolean} [flush_previous]
675674
* @returns {any}
676675
*/
677-
export function flush_sync(fn, flush_previous = true) {
676+
export function flush_sync(fn) {
678677
var previous_scheduler_mode = current_scheduler_mode;
679678
var previous_queued_root_effects = current_queued_root_effects;
680679

@@ -688,9 +687,7 @@ export function flush_sync(fn, flush_previous = true) {
688687
current_queued_root_effects = root_effects;
689688
is_micro_task_queued = false;
690689

691-
if (flush_previous) {
692-
flush_queued_root_effects(previous_queued_root_effects);
693-
}
690+
flush_queued_root_effects(previous_queued_root_effects);
694691

695692
var result = fn?.();
696693

packages/svelte/src/legacy/legacy-client.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { mutable_source, set } from '../internal/client/reactivity/sources.js';
33
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
44
import { hydrate, mount, unmount } from '../internal/client/render.js';
5-
import { get } from '../internal/client/runtime.js';
5+
import { flush_sync, get } from '../internal/client/runtime.js';
66
import { define_property } from '../internal/shared/utils.js';
77

88
/**
@@ -110,6 +110,8 @@ class Svelte4Component {
110110
recover: options.recover
111111
});
112112

113+
flush_sync();
114+
113115
this.#events = props.$$events;
114116

115117
for (const key of Object.keys(this.#instance)) {

packages/svelte/tests/hydration/test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { suite, assert_ok, type BaseTest } from '../suite.js';
88
import { createClassComponent } from 'svelte/legacy';
99
import { render } from 'svelte/server';
1010
import type { CompileOptions } from '#compiler';
11+
import { flushSync } from 'svelte';
1112

1213
interface HydrationTest extends BaseTest {
1314
load_compiled?: boolean;
@@ -114,6 +115,7 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
114115

115116
if (!override) {
116117
const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
118+
flushSync();
117119
assert.equal(target.innerHTML.trim(), expected.trim());
118120
}
119121

packages/svelte/tests/runtime-browser/driver.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import config from '__CONFIG__';
55
// @ts-expect-error
66
import * as assert from 'assert.js';
77
import { createClassComponent } from 'svelte/legacy';
8+
import { flushSync } from 'svelte';
89

910
/** @param {HTMLElement} target */
1011
export default async function (target) {
@@ -45,6 +46,8 @@ export default async function (target) {
4546
} while (new Date().getTime() <= start + ms);
4647
};
4748

49+
flushSync();
50+
4851
if (config.html) {
4952
assert.htmlEqual(target.innerHTML, config.html);
5053
}

packages/svelte/tests/runtime-runes/samples/hydrate-modified-input-group/_config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { flushSync } from 'svelte';
12
import { test } from '../../test';
23

34
export default test({
@@ -9,6 +10,7 @@ export default test({
910
inputs[1].dispatchEvent(new window.Event('change'));
1011
// Hydration shouldn't reset the value to 1
1112
hydrate();
13+
flushSync();
1214

1315
assert.htmlEqual(
1416
target.innerHTML,

packages/svelte/tests/runtime-runes/samples/hydrate-modified-input/_config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { flushSync } from 'svelte';
12
import { test } from '../../test';
23

34
export default test({
@@ -9,6 +10,7 @@ export default test({
910
input.dispatchEvent(new window.Event('input'));
1011
// Hydration shouldn't reset the value to empty
1112
hydrate();
13+
flushSync();
1214

1315
assert.htmlEqual(target.innerHTML, '<input type="text">\nfoo');
1416
}

sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ const app = mount(App, {
4444
});
4545
```
4646

47+
Note that unlike calling `new App(...)` in Svelte 4, things like effects (including `onMount` callbacks, and action functions) will not run during `mount`. If you need to force pending effects to run (in the context of a test, for example) you can do so with `flushSync()`.
48+
4749
### `hydrate`
4850

4951
Like `mount`, but will reuse up any HTML rendered by Svelte's SSR output (from the [`render`](#svelte-server-render) function) inside the target and make it interactive:
@@ -59,6 +61,8 @@ const app = hydrate(App, {
5961
});
6062
```
6163

64+
As with `mount`, effects will not run during `hydrate` — use `flushSync()` immediately afterwards if you need them to.
65+
6266
### `unmount`
6367

6468
Unmounts a component created with [`mount`](#svelte-mount) or [`hydrate`](#svelte-hydrate):

0 commit comments

Comments
 (0)