breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount` (#10516)

* breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount`

closes #9827

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

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

---------

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
pull/10519/head
Simon H 1 year ago committed by GitHub
parent 11b69459b9
commit 2755401034
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount`

@ -43,6 +43,7 @@ import {
untrack, untrack,
effect, effect,
flushSync, flushSync,
flush_sync,
safe_not_equal, safe_not_equal,
current_block, current_block,
managed_effect, managed_effect,
@ -64,12 +65,11 @@ import {
get_descriptors, get_descriptors,
is_array, is_array,
is_function, is_function,
object_assign, object_assign
object_keys
} from './utils.js'; } from './utils.js';
import { is_promise } from '../common.js'; import { is_promise } from '../common.js';
import { bind_transition, trigger_transitions } from './transitions.js'; import { bind_transition, trigger_transitions } from './transitions.js';
import { STATE_SYMBOL, proxy } from './proxy.js'; import { STATE_SYMBOL } from './proxy.js';
/** @type {Set<string>} */ /** @type {Set<string>} */
const all_registerd_events = new Set(); const all_registerd_events = new Set();
@ -2825,14 +2825,21 @@ export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler); return new Proxy({ props }, spread_props_handler);
} }
// TODO 5.0 remove this
/** /**
* Mounts the given component to the given target and returns a handle to the component's public accessors * @deprecated Use `mount` or `hydrate` instead
* as well as a `$set` and `$destroy` method to update the props of the component or destroy it. */
* export function createRoot() {
* If you don't need to interact with the component after mounting, use `mount` instead to save some bytes. throw new Error(
'`createRoot` has been removed. Use `mount` or `hydrate` instead. See the updated docs for more info: https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes'
);
}
/**
* Mounts a component to the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component
* *
* @template {Record<string, any>} Props * @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports * @template {Record<string, any>} Exports
* @template {Record<string, any>} Events * @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component * @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{ * @param {{
@ -2841,48 +2848,22 @@ export function spread_props(...props) {
* events?: Events; * events?: Events;
* context?: Map<any, any>; * context?: Map<any, any>;
* intro?: boolean; * intro?: boolean;
* recover?: false;
* }} options * }} options
* @returns {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} * @returns {Exports}
*/ */
export function createRoot(component, options) { export function mount(component, options) {
const props = proxy(/** @type {any} */ (options.props) || {}, false); init_operations();
const anchor = empty();
let [accessors, $destroy] = hydrate(component, { ...options, props }); options.target.appendChild(anchor);
// Don't flush previous effects to ensure order of outer effects stays consistent
const result = return flush_sync(() => _mount(component, { ...options, anchor }), false);
/** @type {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} */ ({
$set: (next) => {
object_assign(props, next);
},
$destroy
});
for (const key of object_keys(accessors || {})) {
define_property(result, key, {
get() {
// @ts-expect-error TS doesn't know key exists on accessor
return accessors[key];
},
/** @param {any} value */
set(value) {
// @ts-expect-error TS doesn't know key exists on accessor
flushSync(() => (accessors[key] = value));
},
enumerable: true
});
}
return result;
} }
/** /**
* Mounts the given component to the given target and returns the accessors of the component and a function to destroy it. * Hydrates a component on the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component
*
* If you need to interact with the component after mounting, use `createRoot` instead.
* *
* @template {Record<string, any>} Props * @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports * @template {Record<string, any>} Exports
* @template {Record<string, any>} Events * @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component * @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{ * @param {{
@ -2891,19 +2872,65 @@ export function createRoot(component, options) {
* events?: Events; * events?: Events;
* context?: Map<any, any>; * context?: Map<any, any>;
* intro?: boolean; * intro?: boolean;
* recover?: false;
* }} options * }} options
* @returns {[Exports, () => void]} * @returns {Exports}
*/ */
export function mount(component, options) { export function hydrate(component, options) {
init_operations(); init_operations();
const anchor = empty(); const container = options.target;
options.target.appendChild(anchor); const first_child = /** @type {ChildNode} */ (container.firstChild);
return _mount(component, { ...options, anchor }); // Call with insert_text == true to prevent empty {expressions} resulting in an empty
// fragment array, resulting in a hydration error down the line
const hydration_fragment = get_hydration_fragment(first_child, true);
const previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
/** @type {null | Text} */
let anchor = null;
if (hydration_fragment === null) {
anchor = empty();
container.appendChild(anchor);
}
let finished_hydrating = false;
try {
// Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => {
const instance = _mount(component, { ...options, anchor });
// flush_sync will run this callback and then synchronously run any pending effects,
// which don't belong to the hydration phase anymore - therefore reset it here
set_current_hydration_fragment(null);
finished_hydrating = true;
return instance;
}, false);
} catch (error) {
if (!finished_hydrating && options.recover !== false && hydration_fragment !== null) {
// eslint-disable-next-line no-console
console.error(
'ERR_SVELTE_HYDRATION_MISMATCH' +
(DEV
? ': Hydration failed because the initial UI does not match what was rendered on the server.'
: ''),
error
);
remove(hydration_fragment);
first_child.remove();
hydration_fragment.at(-1)?.nextSibling?.remove();
set_current_hydration_fragment(null);
return mount(component, options);
} else {
throw error;
}
} finally {
set_current_hydration_fragment(previous_hydration_fragment);
}
} }
/** /**
* @template {Record<string, any>} Props * @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports * @template {Record<string, any>} Exports
* @template {Record<string, any>} Events * @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component * @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{ * @param {{
@ -2915,7 +2942,7 @@ export function mount(component, options) {
* intro?: boolean; * intro?: boolean;
* recover?: false; * recover?: false;
* }} options * }} options
* @returns {[Exports, () => void]} * @returns {Exports}
*/ */
function _mount(component, options) { function _mount(component, options) {
const registered_events = new Set(); const registered_events = new Set();
@ -2934,7 +2961,7 @@ function _mount(component, options) {
options.context; options.context;
} }
// @ts-expect-error the public typings are not what the actual function looks like // @ts-expect-error the public typings are not what the actual function looks like
accessors = component(options.anchor, options.props || {}); accessors = component(options.anchor, options.props || {}) || {};
if (options.context) { if (options.context) {
pop(); pop();
} }
@ -2981,80 +3008,38 @@ function _mount(component, options) {
event_handle(array_from(all_registerd_events)); event_handle(array_from(all_registerd_events));
root_event_handles.add(event_handle); root_event_handles.add(event_handle);
return [ mounted_components.set(accessors, () => {
accessors, for (const event_name of registered_events) {
() => { container.removeEventListener(event_name, bound_event_listener);
for (const event_name of registered_events) { }
container.removeEventListener(event_name, bound_event_listener); root_event_handles.delete(event_handle);
} const dom = block.d;
root_event_handles.delete(event_handle); if (dom !== null) {
const dom = block.d; remove(dom);
if (dom !== null) {
remove(dom);
}
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
} }
]; destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
});
return accessors;
} }
/** /**
* Hydrates the given component to the given target and returns the accessors of the component and a function to destroy it. * References of the accessors of all components that were `mount`ed or `hydrate`d.
* * Uses a `WeakMap` to avoid memory leaks.
* If you need to interact with the component after hydrating, use `createRoot` instead.
*
* @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{
* target: Node;
* props?: Props;
* events?: Events;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* }} options
* @returns {[Exports, () => void]}
*/ */
export function hydrate(component, options) { let mounted_components = new WeakMap();
init_operations();
const container = options.target;
const first_child = /** @type {ChildNode} */ (container.firstChild);
// Call with insert_text == true to prevent empty {expressions} resulting in an empty
// fragment array, resulting in a hydration error down the line
const hydration_fragment = get_hydration_fragment(first_child, true);
const previous_hydration_fragment = current_hydration_fragment;
try { /**
/** @type {null | Text} */ * Unmounts a component that was previously mounted using `mount` or `hydrate`.
let anchor = null; * @param {Record<string, any>} component
if (hydration_fragment === null) { */
anchor = empty(); export function unmount(component) {
container.appendChild(anchor); const destroy = mounted_components.get(component);
} if (DEV && !destroy) {
set_current_hydration_fragment(hydration_fragment); // eslint-disable-next-line no-console
return _mount(component, { ...options, anchor }); console.warn('Tried to unmount a component that was not mounted.');
} catch (error) {
if (options.recover !== false && hydration_fragment !== null) {
// eslint-disable-next-line no-console
console.error(
'ERR_SVELTE_HYDRATION_MISMATCH' +
(DEV
? ': Hydration failed because the initial UI does not match what was rendered on the server.'
: ''),
error
);
remove(hydration_fragment);
first_child.remove();
hydration_fragment.at(-1)?.nextSibling?.remove();
set_current_hydration_fragment(null);
return mount(component, options);
} else {
throw error;
}
} finally {
set_current_hydration_fragment(previous_hydration_fragment);
} }
destroy?.();
} }
/** /**

@ -749,9 +749,22 @@ export function flush_local_pre_effects(context) {
* @returns {void} * @returns {void}
*/ */
export function flushSync(fn) { export function flushSync(fn) {
flush_sync(fn);
}
/**
* 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) {
const previous_scheduler_mode = current_scheduler_mode; const previous_scheduler_mode = current_scheduler_mode;
const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects; const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects;
const previous_queued_effects = current_queued_effects; const previous_queued_effects = current_queued_effects;
let result;
try { try {
infinite_loop_guard(); infinite_loop_guard();
/** @type {import('./types.js').EffectSignal[]} */ /** @type {import('./types.js').EffectSignal[]} */
@ -762,10 +775,12 @@ export function flushSync(fn) {
current_scheduler_mode = FLUSH_SYNC; current_scheduler_mode = FLUSH_SYNC;
current_queued_pre_and_render_effects = pre_and_render_effects; current_queued_pre_and_render_effects = pre_and_render_effects;
current_queued_effects = effects; current_queued_effects = effects;
flush_queued_effects(previous_queued_pre_and_render_effects); if (flush_previous) {
flush_queued_effects(previous_queued_effects); flush_queued_effects(previous_queued_pre_and_render_effects);
flush_queued_effects(previous_queued_effects);
}
if (fn !== undefined) { if (fn !== undefined) {
fn(); result = fn();
} }
if (current_queued_pre_and_render_effects.length > 0 || effects.length > 0) { if (current_queued_pre_and_render_effects.length > 0 || effects.length > 0) {
flushSync(); flushSync();
@ -782,6 +797,8 @@ export function flushSync(fn) {
current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects; current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects;
current_queued_effects = previous_queued_effects; current_queued_effects = previous_queued_effects;
} }
return result;
} }
/** /**

@ -14,6 +14,7 @@ import * as $ from '../internal/index.js';
* @param {import('../main/public.js').ComponentConstructorOptions<Props> & { * @param {import('../main/public.js').ComponentConstructorOptions<Props> & {
* component: import('../main/public.js').SvelteComponent<Props, Events, Slots>; * component: import('../main/public.js').SvelteComponent<Props, Events, Slots>;
* immutable?: boolean; * immutable?: boolean;
* hydrate?: boolean;
* recover?: false; * recover?: false;
* }} options * }} options
* @returns {import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports} * @returns {import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports}
@ -53,28 +54,28 @@ class Svelte4Component {
/** @type {any} */ /** @type {any} */
#events = {}; #events = {};
/** @type {ReturnType<typeof $.createRoot>} */ /** @type {Record<string, any>} */
#instance; #instance;
/** /**
* @param {import('../main/public.js').ComponentConstructorOptions & { * @param {import('../main/public.js').ComponentConstructorOptions & {
* component: any; * component: any;
* immutable?: boolean; * immutable?: boolean;
* hydrate?: boolean;
* recover?: false; * recover?: false;
* }} options * }} options
*/ */
constructor(options) { constructor(options) {
this.#instance = $.createRoot(options.component, { const props = $.proxy({ ...(options.props || {}), $$events: this.#events }, false);
this.#instance = (options.hydrate ? $.hydrate : $.mount)(options.component, {
target: options.target, target: options.target,
props: { ...options.props, $$events: this.#events }, props,
context: options.context, context: options.context,
intro: options.intro, intro: options.intro,
recover: options.recover recover: options.recover
}); });
for (const key of Object.keys(this.#instance)) { for (const key of Object.keys(this.#instance)) {
if (key === '$set' || key === '$destroy') continue;
define_property(this, key, { define_property(this, key, {
get() { get() {
return this.#instance[key]; return this.#instance[key];
@ -86,6 +87,13 @@ class Svelte4Component {
enumerable: true enumerable: true
}); });
} }
this.#instance.$set = /** @param {Record<string, any>} next */ (next) => {
Object.assign(props, next);
};
this.#instance.$destroy = () => {
$.unmount(this.#instance);
};
} }
/** @param {Record<string, any>} props */ /** @param {Record<string, any>} props */

@ -235,10 +235,11 @@ function init_update_callbacks(context) {
// (except probably untrack — do we want to expose that, if there's also a rune?) // (except probably untrack — do we want to expose that, if there's also a rune?)
export { export {
flushSync, flushSync,
createRoot,
mount, mount,
hydrate, hydrate,
tick, tick,
unmount,
untrack, untrack,
unstate unstate,
createRoot
} from '../internal/index.js'; } from '../internal/index.js';

@ -1,7 +1,6 @@
import { on_destroy } from '../internal/server/index.js'; import { on_destroy } from '../internal/server/index.js';
export { export {
createRoot,
createEventDispatcher, createEventDispatcher,
flushSync, flushSync,
getAllContexts, getAllContexts,
@ -11,7 +10,9 @@ export {
hydrate, hydrate,
setContext, setContext,
tick, tick,
untrack unmount,
untrack,
createRoot
} from './main-client.js'; } from './main-client.js';
/** @returns {void} */ /** @returns {void} */

@ -4,7 +4,7 @@ import * as fs from 'node:fs';
import { assert } from 'vitest'; import { assert } from 'vitest';
import { compile_directory, try_read_file } from '../helpers.js'; import { compile_directory, try_read_file } from '../helpers.js';
import { assert_html_equal } from '../html_equal.js'; import { assert_html_equal } from '../html_equal.js';
import { createRoot } from 'svelte'; import { mount, unmount } from 'svelte';
import { suite, type BaseTest } from '../suite.js'; import { suite, type BaseTest } from '../suite.js';
import type { CompileOptions, Warning } from '#compiler'; import type { CompileOptions, Warning } from '#compiler';
@ -55,7 +55,7 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
if (expected.html !== null) { if (expected.html !== null) {
const target = window.document.createElement('main'); const target = window.document.createElement('main');
const { $destroy } = createRoot(ClientComponent, { props: config.props ?? {}, target }); const component = mount(ClientComponent, { props: config.props ?? {}, target });
const html = target.innerHTML; const html = target.innerHTML;
@ -63,7 +63,7 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
assert_html_equal(html, expected.html); assert_html_equal(html, expected.html);
$destroy(); unmount(component);
window.document.head.innerHTML = ''; // remove added styles window.document.head.innerHTML = ''; // remove added styles
// TODO enable SSR tests // TODO enable SSR tests

@ -0,0 +1,2 @@
<!--ssr:0--><noscript>JavaScript is required for this site.</noscript>
<h1>Hello!</h1><p>Count: 1</p><!--ssr:0-->

@ -84,6 +84,7 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
const component = createClassComponent({ const component = createClassComponent({
component: (await import(`${cwd}/_output/client/main.svelte.js`)).default, component: (await import(`${cwd}/_output/client/main.svelte.js`)).default,
target, target,
hydrate: true,
props: config.props props: config.props
}); });

@ -5,11 +5,12 @@ export default test({
async test({ assert, target }) { async test({ assert, target }) {
target.innerHTML = '<my-app prop/>'; target.innerHTML = '<my-app prop/>';
await tick(); await tick();
await tick();
await tick();
/** @type {any} */ /** @type {any} */
const el = target.querySelector('my-app'); const el = target.querySelector('my-app');
await tick();
assert.ok(el.wasCreated); assert.ok(el.wasCreated);
assert.ok(el.propsInitialized); assert.ok(el.propsInitialized);
} }

@ -24,12 +24,13 @@ export default async function (target) {
component: SvelteComponent, component: SvelteComponent,
target, target,
props: config.props, props: config.props,
intro: config.intro intro: config.intro,
hydrate: __HYDRATE__
}, },
config.options || {} config.options || {}
); );
const component = createClassComponent(options); const component = __CE_TEST__ ? null : createClassComponent(options);
/** /**
* @param {() => boolean} fn * @param {() => boolean} fn
@ -50,14 +51,19 @@ export default async function (target) {
if (config.test) { if (config.test) {
await config.test({ await config.test({
assert, assert,
component, get component() {
if (!component) {
throw new Error('test property `component` is not available in custom element tests');
}
return component;
},
componentCtor: SvelteComponent, componentCtor: SvelteComponent,
target, target,
window, window,
waitUntil: wait_until waitUntil: wait_until
}); });
component.$destroy(); component?.$destroy();
if (unhandled_rejection) { if (unhandled_rejection) {
throw unhandled_rejection; throw unhandled_rejection;

@ -67,6 +67,10 @@ async function run_test(
const build_result = await build({ const build_result = await build({
entryPoints: [`${__dirname}/driver.js`], entryPoints: [`${__dirname}/driver.js`],
write: false, write: false,
define: {
__HYDRATE__: String(hydrate),
__CE_TEST__: String(test_dir.includes('custom-elements-samples'))
},
alias: { alias: {
__MAIN_DOT_SVELTE__: path.resolve(test_dir, 'main.svelte'), __MAIN_DOT_SVELTE__: path.resolve(test_dir, 'main.svelte'),
__CONFIG__: path.resolve(test_dir, '_config.js'), __CONFIG__: path.resolve(test_dir, '_config.js'),

@ -1,5 +1,6 @@
<script> <script>
import { onMount, onDestroy, tick, createRoot } from 'svelte'; import { onMount, onDestroy, tick } from 'svelte';
import { createClassComponent } from 'svelte/legacy'
export let component; export let component;
@ -14,7 +15,8 @@ function mountComponent(doc) {
if (content) content.$destroy(); if (content) content.$destroy();
if (doc && component) { if (doc && component) {
const { component, ...props } = $$props; const { component, ...props } = $$props;
content = createRoot(component, { target: doc.body, props }); // When this test is migrated to runes, use mount/unmount and $state for updating props instead
content = createClassComponent({ component, target: doc.body, props });
} }
} }

@ -15,6 +15,7 @@ export default test({
component.visible = false; component.visible = false;
raf.tick(26); raf.tick(26);
assert.equal(div.style.opacity, '0.16666'); // The exact number doesn't matter here, this test is about ensuring that transitions work in iframes
assert.equal(Number(div.style.opacity).toFixed(4), '0.8333');
} }
}); });

@ -258,7 +258,8 @@ async function run_test_variant(
target, target,
immutable: config.immutable, immutable: config.immutable,
intro: config.intro, intro: config.intro,
recover: false recover: false,
hydrate: variant === 'hydrate'
}); });
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

@ -1,11 +1,11 @@
import { asClassComponent, createClassComponent } from 'svelte/legacy'; import { asClassComponent, createClassComponent } from 'svelte/legacy';
import { import {
createRoot,
SvelteComponent, SvelteComponent,
type ComponentEvents, type ComponentEvents,
type ComponentProps, type ComponentProps,
type ComponentType, type ComponentType,
mount mount,
hydrate
} from 'svelte'; } from 'svelte';
// --------------------------------------------------------------------------- legacy: classes // --------------------------------------------------------------------------- legacy: classes
@ -119,7 +119,7 @@ mount(NewComponent, {
recover: false recover: false
}); });
const instance = createRoot(NewComponent, { hydrate(NewComponent, {
target: null as any as Document | Element | ShadowRoot | Text | Comment, target: null as any as Document | Element | ShadowRoot | Text | Comment,
props: { props: {
prop: 'foo', prop: 'foo',
@ -133,14 +133,6 @@ const instance = createRoot(NewComponent, {
intro: false, intro: false,
recover: false recover: false
}); });
instance.$set({
prop: 'foo',
// @ts-expect-error
x: ''
});
instance.$set({});
instance.$destroy();
instance.anExport === 1;
// --------------------------------------------------------------------------- interop // --------------------------------------------------------------------------- interop
@ -175,5 +167,6 @@ asLegacyComponent.$$prop_def.x = '';
asLegacyComponent.anExport; asLegacyComponent.anExport;
const x: typeof asLegacyComponent = createClassComponent({ const x: typeof asLegacyComponent = createClassComponent({
target: null as any, target: null as any,
hydrate: true,
component: newComponent component: newComponent
}); });

@ -324,50 +324,36 @@ declare module 'svelte' {
*/ */
type NotFunction<T> = T extends Function ? never : T; type NotFunction<T> = T extends Function ? never : T;
/** /**
* Mounts the given component to the given target and returns a handle to the component's public accessors * @deprecated Use `mount` or `hydrate` instead
* as well as a `$set` and `$destroy` method to update the props of the component or destroy it. */
* export function createRoot(): void;
* If you don't need to interact with the component after mounting, use `mount` instead to save some bytes.
*
* */
export function createRoot<Props extends Record<string, any>, Exports extends Record<string, any> | undefined, Events extends Record<string, any>>(component: ComponentType<SvelteComponent<Props, Events, any>>, options: {
target: Node;
props?: Props | undefined;
events?: Events | undefined;
context?: Map<any, any> | undefined;
intro?: boolean | undefined;
recover?: false | undefined;
}): Exports & {
$destroy: () => void;
$set: (props: Partial<Props>) => void;
};
/** /**
* Mounts the given component to the given target and returns the accessors of the component and a function to destroy it. * Mounts a component to the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component
*
* If you need to interact with the component after mounting, use `createRoot` instead.
* *
* */ * */
export function mount<Props extends Record<string, any>, Exports extends Record<string, any> | undefined, Events extends Record<string, any>>(component: ComponentType<SvelteComponent<Props, Events, any>>, options: { export function mount<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>>(component: ComponentType<SvelteComponent<Props, Events, any>>, options: {
target: Node; target: Node;
props?: Props | undefined; props?: Props | undefined;
events?: Events | undefined; events?: Events | undefined;
context?: Map<any, any> | undefined; context?: Map<any, any> | undefined;
intro?: boolean | undefined; intro?: boolean | undefined;
}): [Exports, () => void]; }): Exports;
/** /**
* Hydrates the given component to the given target and returns the accessors of the component and a function to destroy it. * Hydrates a component on the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component
*
* If you need to interact with the component after hydrating, use `createRoot` instead.
* *
* */ * */
export function hydrate<Props extends Record<string, any>, Exports extends Record<string, any> | undefined, Events extends Record<string, any>>(component: ComponentType<SvelteComponent<Props, Events, any>>, options: { export function hydrate<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>>(component: ComponentType<SvelteComponent<Props, Events, any>>, options: {
target: Node; target: Node;
props?: Props | undefined; props?: Props | undefined;
events?: Events | undefined; events?: Events | undefined;
context?: Map<any, any> | undefined; context?: Map<any, any> | undefined;
intro?: boolean | undefined; intro?: boolean | undefined;
recover?: false | undefined; recover?: false | undefined;
}): [Exports, () => void]; }): Exports;
/**
* Unmounts a component that was previously mounted using `mount` or `hydrate`.
* */
export function unmount(component: Record<string, any>): void;
/** /**
* Synchronously flushes any pending state changes and those that result from it. * Synchronously flushes any pending state changes and those that result from it.
* */ * */
@ -1737,6 +1723,7 @@ declare module 'svelte/legacy' {
export function createClassComponent<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>, Slots extends Record<string, any>>(options: ComponentConstructorOptions<Props> & { export function createClassComponent<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>, Slots extends Record<string, any>>(options: ComponentConstructorOptions<Props> & {
component: SvelteComponent<Props, Events, Slots>; component: SvelteComponent<Props, Events, Slots>;
immutable?: boolean | undefined; immutable?: boolean | undefined;
hydrate?: boolean | undefined;
recover?: false | undefined; recover?: false | undefined;
}): SvelteComponent<Props, Events, Slots> & Exports; }): SvelteComponent<Props, Events, Slots> & Exports;
/** /**

@ -1,8 +1,9 @@
// @ts-ignore // @ts-ignore
import { hydrate } from 'svelte'; import { hydrate, unmount } from 'svelte';
// @ts-ignore you need to create this file // @ts-ignore you need to create this file
import App from './App.svelte'; import App from './App.svelte';
// @ts-ignore const component = hydrate(App, {
[window.unmount] = hydrate(App, {
target: document.getElementById('root')! target: document.getElementById('root')!
}); });
// @ts-ignore
window.unmount = () => unmount(component);

@ -6,23 +6,71 @@ While Svelte 5 is a complete rewrite, we have done our best to ensure that most
## Components are no longer classes ## Components are no longer classes
In Svelte 3 and 4, components are classes. In Svelte 5 they are functions and should be instantiated differently. If you need to manually instantiate components, you should use `mount` or `createRoot` (imported from `svelte`) instead. If you see this error using SvelteKit, try updating to the latest version of SvelteKit first, which adds support for Svelte 5. If you're using Svelte without SvelteKit, you'll likely have a `main.js` file (or similar) which you need to adjust: In Svelte 3 and 4, components are classes. In Svelte 5 they are functions and should be instantiated differently. If you need to manually instantiate components, you should use `mount` or `hydrate` (imported from `svelte`) instead. If you see this error using SvelteKit, try updating to the latest version of SvelteKit first, which adds support for Svelte 5. If you're using Svelte without SvelteKit, you'll likely have a `main.js` file (or similar) which you need to adjust:
```diff ```diff
+ import { createRoot } from 'svelte'; + import { mount } from 'svelte';
import App from './App.svelte' import App from './App.svelte'
- const app = new App({ target: document.getElementById("app") }); - const app = new App({ target: document.getElementById("app") });
+ const app = createRoot(App, { target: document.getElementById("app") }); + const app = mount(App, { target: document.getElementById("app") });
export default app; export default app;
``` ```
`createRoot` returns an object with a `$set` and `$destroy` method on it. It does not come with an `$on` method you may know from the class component API. Instead, pass them via the `events` property on the options argument. If you don't need to interact with the component instance after creating it, you can use `mount` instead, which saves some bytes. `mount` and `hydrate` have the exact same API. The difference is that `hydrate` will pick up the Svelte's server-rendered HTML inside its target and hydrate it. Both return an object with the exports of the component and potentially property accessors (if compiled with `accesors: true`). They do not come with the `$on`, `$set` and `$destroy` methods you may know from the class component API. These are its replacements:
For `$on`, instead of listening to events, pass them via the `events` property on the options argument.
```diff
+ import { mount } from 'svelte';
import App from './App.svelte'
- const app = new App({ target: document.getElementById("app") });
- app.$on('event', callback);
+ const app = mount(App, { target: document.getElementById("app"), events: { event: callback } });
```
> Note that using `events` is discouraged — instead, [use callbacks](https://svelte-5-preview.vercel.app/docs/event-handlers) > Note that using `events` is discouraged — instead, [use callbacks](https://svelte-5-preview.vercel.app/docs/event-handlers)
As a stop-gap-solution, you can also use `createClassComponent` or `asClassComponent` (imported from `svelte/legacy`) instead to keep the same API after instantiating. If this component is not under your control, you can use the `legacy.componentApi` compiler option for auto-applied backwards compatibility (note that this adds a bit of overhead to each component). For `$set`, use `$state` instead to create a reactive property object and manipulate it. If you're doing this inside a `.js` or `.ts` file, adjust the ending to include `.svelte`, i.e. `.svelte.js` or `.svelte.ts`.
```diff
+ import { mount } from 'svelte';
import App from './App.svelte'
- const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
- app.$set('event', { foo: 'baz' });
+ const props = $state({ foo: 'bar' });
+ const app = mount(App, { target: document.getElementById("app"), props });
+ props.foo = 'baz';
```
For `$destroy`, use `unmount` instead.
```diff
+ import { mount, unmount } from 'svelte';
import App from './App.svelte'
- const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } });
- app.$destroy();
+ const app = mount(App, { target: document.getElementById("app") });
+ unmount(app);
```
As a stop-gap-solution, you can also use `createClassComponent` or `asClassComponent` (imported from `svelte/legacy`) instead to keep the same API known from Svelte 4 after instantiating.
```diff
+ import { createClassComponent } from 'svelte/legacy';
import App from './App.svelte'
- const app = new App({ target: document.getElementById("app") });
+ const app = createClassComponent({ component: App, target: document.getElementById("app") });
export default app;
```
If this component is not under your control, you can use the `legacy.componentApi` compiler option for auto-applied backwards compatibility (note that this adds a bit of overhead to each component).
### Server API changes ### Server API changes

Loading…
Cancel
Save