diff --git a/.changeset/slow-gorillas-yawn.md b/.changeset/slow-gorillas-yawn.md new file mode 100644 index 000000000000..376b4d041c4d --- /dev/null +++ b/.changeset/slow-gorillas-yawn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +breaking: avoid flushing queued updates on mount/hydrate diff --git a/documentation/docs/04-runtime/04-imperative-component-api.md b/documentation/docs/04-runtime/04-imperative-component-api.md index 067cb95ef0b3..b4517bd34f93 100644 --- a/documentation/docs/04-runtime/04-imperative-component-api.md +++ b/documentation/docs/04-runtime/04-imperative-component-api.md @@ -29,6 +29,8 @@ const app = mount(App, { 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. +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()`. + ## `unmount` Unmounts a component created with [`mount`](#mount) or [`hydrate`](#hydrate): @@ -74,3 +76,5 @@ const app = hydrate(App, { props: { some: 'property' } }); ``` + +As with `mount`, effects will not run during `hydrate` — use `flushSync()` immediately afterwards if you need them to. diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 4bb01a48993f..7f6b23911419 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -81,8 +81,7 @@ export function set_text(text, value) { */ export function mount(component, options) { const anchor = options.anchor ?? options.target.appendChild(empty()); - // Don't flush previous effects to ensure order of outer effects stays consistent - return flush_sync(() => _mount(component, { ...options, anchor }), false); + return _mount(component, { ...options, anchor }); } /** @@ -115,38 +114,35 @@ export function hydrate(component, options) { const previous_hydrate_node = hydrate_node; try { - // Don't flush previous effects to ensure order of outer effects stays consistent - return flush_sync(() => { - var anchor = /** @type {TemplateNode} */ (target.firstChild); - while ( - anchor && - (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) - ) { - anchor = /** @type {TemplateNode} */ (anchor.nextSibling); - } + var anchor = /** @type {TemplateNode} */ (target.firstChild); + while ( + anchor && + (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) + ) { + anchor = /** @type {TemplateNode} */ (anchor.nextSibling); + } - if (!anchor) { - throw HYDRATION_ERROR; - } + if (!anchor) { + throw HYDRATION_ERROR; + } - set_hydrating(true); - set_hydrate_node(/** @type {Comment} */ (anchor)); - hydrate_next(); + set_hydrating(true); + set_hydrate_node(/** @type {Comment} */ (anchor)); + hydrate_next(); - const instance = _mount(component, { ...options, anchor }); + const instance = _mount(component, { ...options, anchor }); - if ( - hydrate_node.nodeType !== 8 || - /** @type {Comment} */ (hydrate_node).data !== HYDRATION_END - ) { - w.hydration_mismatch(); - throw HYDRATION_ERROR; - } + if ( + hydrate_node.nodeType !== 8 || + /** @type {Comment} */ (hydrate_node).data !== HYDRATION_END + ) { + w.hydration_mismatch(); + throw HYDRATION_ERROR; + } - set_hydrating(false); + set_hydrating(false); - return instance; - }, false); + return /** @type {Exports} */ (instance); } catch (error) { if (error === HYDRATION_ERROR) { // TODO it's possible for event listeners to have been added and diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 16facffabd3f..779548a65cdd 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -671,10 +671,9 @@ function process_effects(effect, collected_effects) { * Internal version of `flushSync` with the option to not flush previous effects. * Returns the result of the passed function, if given. * @param {() => any} [fn] - * @param {boolean} [flush_previous] * @returns {any} */ -export function flush_sync(fn, flush_previous = true) { +export function flush_sync(fn) { var previous_scheduler_mode = current_scheduler_mode; var previous_queued_root_effects = current_queued_root_effects; @@ -688,9 +687,7 @@ export function flush_sync(fn, flush_previous = true) { current_queued_root_effects = root_effects; is_micro_task_queued = false; - if (flush_previous) { - flush_queued_root_effects(previous_queued_root_effects); - } + flush_queued_root_effects(previous_queued_root_effects); var result = fn?.(); diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 45927edc7b81..a11f3f9ceedc 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -2,7 +2,7 @@ import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { hydrate, mount, unmount } from '../internal/client/render.js'; -import { get } from '../internal/client/runtime.js'; +import { flush_sync, get } from '../internal/client/runtime.js'; import { define_property } from '../internal/shared/utils.js'; /** @@ -110,6 +110,8 @@ class Svelte4Component { recover: options.recover }); + flush_sync(); + this.#events = props.$$events; for (const key of Object.keys(this.#instance)) { diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index fab0e5b30807..d592a65de3d8 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -8,6 +8,7 @@ import { suite, assert_ok, type BaseTest } from '../suite.js'; import { createClassComponent } from 'svelte/legacy'; import { render } from 'svelte/server'; import type { CompileOptions } from '#compiler'; +import { flushSync } from 'svelte'; interface HydrationTest extends BaseTest { load_compiled?: boolean; @@ -114,6 +115,7 @@ const { test, run } = suite(async (config, cwd) => { if (!override) { const expected = read(`${cwd}/_expected.html`) ?? rendered.html; + flushSync(); assert.equal(target.innerHTML.trim(), expected.trim()); } diff --git a/packages/svelte/tests/runtime-browser/driver.js b/packages/svelte/tests/runtime-browser/driver.js index 7a5603e9b8e4..ef6acd08f636 100644 --- a/packages/svelte/tests/runtime-browser/driver.js +++ b/packages/svelte/tests/runtime-browser/driver.js @@ -5,6 +5,7 @@ import config from '__CONFIG__'; // @ts-expect-error import * as assert from 'assert.js'; import { createClassComponent } from 'svelte/legacy'; +import { flushSync } from 'svelte'; /** @param {HTMLElement} target */ export default async function (target) { @@ -45,6 +46,8 @@ export default async function (target) { } while (new Date().getTime() <= start + ms); }; + flushSync(); + if (config.html) { assert.htmlEqual(target.innerHTML, config.html); } diff --git a/packages/svelte/tests/runtime-runes/samples/hydrate-modified-input-group/_config.js b/packages/svelte/tests/runtime-runes/samples/hydrate-modified-input-group/_config.js index 7f3bfac70746..7596fd97be52 100644 --- a/packages/svelte/tests/runtime-runes/samples/hydrate-modified-input-group/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/hydrate-modified-input-group/_config.js @@ -1,3 +1,4 @@ +import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ @@ -9,6 +10,7 @@ export default test({ inputs[1].dispatchEvent(new window.Event('change')); // Hydration shouldn't reset the value to 1 hydrate(); + flushSync(); assert.htmlEqual( target.innerHTML, diff --git a/packages/svelte/tests/runtime-runes/samples/hydrate-modified-input/_config.js b/packages/svelte/tests/runtime-runes/samples/hydrate-modified-input/_config.js index e5bbe5b0fe4f..10dac59fae7e 100644 --- a/packages/svelte/tests/runtime-runes/samples/hydrate-modified-input/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/hydrate-modified-input/_config.js @@ -1,3 +1,4 @@ +import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ @@ -9,6 +10,7 @@ export default test({ input.dispatchEvent(new window.Event('input')); // Hydration shouldn't reset the value to empty hydrate(); + flushSync(); assert.htmlEqual(target.innerHTML, '\nfoo'); } diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md index d364f8242942..a42cde231918 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md @@ -44,6 +44,8 @@ const app = mount(App, { }); ``` +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()`. + ### `hydrate` 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, { }); ``` +As with `mount`, effects will not run during `hydrate` — use `flushSync()` immediately afterwards if you need them to. + ### `unmount` Unmounts a component created with [`mount`](#svelte-mount) or [`hydrate`](#svelte-hydrate):