breaking: avoid flushing queued updates on mount/hydrate (#12602)

* Revert "Revert "breaking: avoid flushing queued updates on mount/hydrate" (#1…"

This reverts commit 8d139210b7.

* fix legacy wrapper

* lint

* docs

* duplicate

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12605/head
Dominic Gannaway 1 year ago committed by GitHub
parent 75ea6da9cd
commit afa3128d2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: avoid flushing queued updates on mount/hydrate

@ -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. 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` ## `unmount`
Unmounts a component created with [`mount`](#mount) or [`hydrate`](#hydrate): Unmounts a component created with [`mount`](#mount) or [`hydrate`](#hydrate):
@ -74,3 +76,5 @@ const app = hydrate(App, {
props: { some: 'property' } props: { some: 'property' }
}); });
``` ```
As with `mount`, effects will not run during `hydrate` — use `flushSync()` immediately afterwards if you need them to.

@ -81,8 +81,7 @@ export function set_text(text, value) {
*/ */
export function mount(component, options) { export function mount(component, options) {
const anchor = options.anchor ?? options.target.appendChild(empty()); const anchor = options.anchor ?? options.target.appendChild(empty());
// Don't flush previous effects to ensure order of outer effects stays consistent return _mount(component, { ...options, anchor });
return flush_sync(() => _mount(component, { ...options, anchor }), false);
} }
/** /**
@ -115,38 +114,35 @@ export function hydrate(component, options) {
const previous_hydrate_node = hydrate_node; const previous_hydrate_node = hydrate_node;
try { try {
// Don't flush previous effects to ensure order of outer effects stays consistent var anchor = /** @type {TemplateNode} */ (target.firstChild);
return flush_sync(() => { while (
var anchor = /** @type {TemplateNode} */ (target.firstChild); anchor &&
while ( (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
anchor && ) {
(anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) anchor = /** @type {TemplateNode} */ (anchor.nextSibling);
) { }
anchor = /** @type {TemplateNode} */ (anchor.nextSibling);
}
if (!anchor) { if (!anchor) {
throw HYDRATION_ERROR; throw HYDRATION_ERROR;
} }
set_hydrating(true); set_hydrating(true);
set_hydrate_node(/** @type {Comment} */ (anchor)); set_hydrate_node(/** @type {Comment} */ (anchor));
hydrate_next(); hydrate_next();
const instance = _mount(component, { ...options, anchor }); const instance = _mount(component, { ...options, anchor });
if ( if (
hydrate_node.nodeType !== 8 || hydrate_node.nodeType !== 8 ||
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END /** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
) { ) {
w.hydration_mismatch(); w.hydration_mismatch();
throw HYDRATION_ERROR; throw HYDRATION_ERROR;
} }
set_hydrating(false); set_hydrating(false);
return instance; return /** @type {Exports} */ (instance);
}, false);
} catch (error) { } catch (error) {
if (error === HYDRATION_ERROR) { if (error === HYDRATION_ERROR) {
// TODO it's possible for event listeners to have been added and // TODO it's possible for event listeners to have been added and

@ -671,10 +671,9 @@ function process_effects(effect, collected_effects) {
* Internal version of `flushSync` with the option to not flush previous effects. * Internal version of `flushSync` with the option to not flush previous effects.
* Returns the result of the passed function, if given. * Returns the result of the passed function, if given.
* @param {() => any} [fn] * @param {() => any} [fn]
* @param {boolean} [flush_previous]
* @returns {any} * @returns {any}
*/ */
export function flush_sync(fn, flush_previous = true) { export function flush_sync(fn) {
var previous_scheduler_mode = current_scheduler_mode; var previous_scheduler_mode = current_scheduler_mode;
var previous_queued_root_effects = current_queued_root_effects; 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; current_queued_root_effects = root_effects;
is_micro_task_queued = false; 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?.(); var result = fn?.();

@ -2,7 +2,7 @@
import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { hydrate, mount, unmount } from '../internal/client/render.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'; import { define_property } from '../internal/shared/utils.js';
/** /**
@ -110,6 +110,8 @@ class Svelte4Component {
recover: options.recover recover: options.recover
}); });
flush_sync();
this.#events = props.$$events; this.#events = props.$$events;
for (const key of Object.keys(this.#instance)) { for (const key of Object.keys(this.#instance)) {

@ -8,6 +8,7 @@ import { suite, assert_ok, type BaseTest } from '../suite.js';
import { createClassComponent } from 'svelte/legacy'; import { createClassComponent } from 'svelte/legacy';
import { render } from 'svelte/server'; import { render } from 'svelte/server';
import type { CompileOptions } from '#compiler'; import type { CompileOptions } from '#compiler';
import { flushSync } from 'svelte';
interface HydrationTest extends BaseTest { interface HydrationTest extends BaseTest {
load_compiled?: boolean; load_compiled?: boolean;
@ -114,6 +115,7 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
if (!override) { if (!override) {
const expected = read(`${cwd}/_expected.html`) ?? rendered.html; const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
flushSync();
assert.equal(target.innerHTML.trim(), expected.trim()); assert.equal(target.innerHTML.trim(), expected.trim());
} }

@ -5,6 +5,7 @@ import config from '__CONFIG__';
// @ts-expect-error // @ts-expect-error
import * as assert from 'assert.js'; import * as assert from 'assert.js';
import { createClassComponent } from 'svelte/legacy'; import { createClassComponent } from 'svelte/legacy';
import { flushSync } from 'svelte';
/** @param {HTMLElement} target */ /** @param {HTMLElement} target */
export default async function (target) { export default async function (target) {
@ -45,6 +46,8 @@ export default async function (target) {
} while (new Date().getTime() <= start + ms); } while (new Date().getTime() <= start + ms);
}; };
flushSync();
if (config.html) { if (config.html) {
assert.htmlEqual(target.innerHTML, config.html); assert.htmlEqual(target.innerHTML, config.html);
} }

@ -1,3 +1,4 @@
import { flushSync } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
@ -9,6 +10,7 @@ export default test({
inputs[1].dispatchEvent(new window.Event('change')); inputs[1].dispatchEvent(new window.Event('change'));
// Hydration shouldn't reset the value to 1 // Hydration shouldn't reset the value to 1
hydrate(); hydrate();
flushSync();
assert.htmlEqual( assert.htmlEqual(
target.innerHTML, target.innerHTML,

@ -1,3 +1,4 @@
import { flushSync } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
@ -9,6 +10,7 @@ export default test({
input.dispatchEvent(new window.Event('input')); input.dispatchEvent(new window.Event('input'));
// Hydration shouldn't reset the value to empty // Hydration shouldn't reset the value to empty
hydrate(); hydrate();
flushSync();
assert.htmlEqual(target.innerHTML, '<input type="text">\nfoo'); assert.htmlEqual(target.innerHTML, '<input type="text">\nfoo');
} }

@ -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` ### `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: 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` ### `unmount`
Unmounts a component created with [`mount`](#svelte-mount) or [`hydrate`](#svelte-hydrate): Unmounts a component created with [`mount`](#svelte-mount) or [`hydrate`](#svelte-hydrate):

Loading…
Cancel
Save