Skip to content

Commit a9fbe4d

Browse files
committed
breaking: remove createRoot, adjust mount/hydrate APIs, introduce unmount
closes #9827
1 parent 11b6945 commit a9fbe4d

File tree

19 files changed

+254
-189
lines changed

19 files changed

+254
-189
lines changed

.changeset/quiet-apricots-dream.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount`

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

+103-118
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
untrack,
4444
effect,
4545
flushSync,
46+
flush_sync,
4647
safe_not_equal,
4748
current_block,
4849
managed_effect,
@@ -64,12 +65,11 @@ import {
6465
get_descriptors,
6566
is_array,
6667
is_function,
67-
object_assign,
68-
object_keys
68+
object_assign
6969
} from './utils.js';
7070
import { is_promise } from '../common.js';
7171
import { bind_transition, trigger_transitions } from './transitions.js';
72-
import { STATE_SYMBOL, proxy } from './proxy.js';
72+
import { STATE_SYMBOL } from './proxy.js';
7373

7474
/** @type {Set<string>} */
7575
const all_registerd_events = new Set();
@@ -2825,14 +2825,21 @@ export function spread_props(...props) {
28252825
return new Proxy({ props }, spread_props_handler);
28262826
}
28272827

2828+
// TODO 5.0 remove this
28282829
/**
2829-
* Mounts the given component to the given target and returns a handle to the component's public accessors
2830-
* as well as a `$set` and `$destroy` method to update the props of the component or destroy it.
2831-
*
2832-
* If you don't need to interact with the component after mounting, use `mount` instead to save some bytes.
2830+
* @deprecated Use `mount` or `hydrate` instead
2831+
*/
2832+
export function createRoot() {
2833+
throw new Error(
2834+
'`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'
2835+
);
2836+
}
2837+
2838+
/**
2839+
* Mounts a component to the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component
28332840
*
28342841
* @template {Record<string, any>} Props
2835-
* @template {Record<string, any> | undefined} Exports
2842+
* @template {Record<string, any>} Exports
28362843
* @template {Record<string, any>} Events
28372844
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
28382845
* @param {{
@@ -2841,48 +2848,22 @@ export function spread_props(...props) {
28412848
* events?: Events;
28422849
* context?: Map<any, any>;
28432850
* intro?: boolean;
2844-
* recover?: false;
28452851
* }} options
2846-
* @returns {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }}
2852+
* @returns {Exports}
28472853
*/
2848-
export function createRoot(component, options) {
2849-
const props = proxy(/** @type {any} */ (options.props) || {}, false);
2850-
2851-
let [accessors, $destroy] = hydrate(component, { ...options, props });
2852-
2853-
const result =
2854-
/** @type {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} */ ({
2855-
$set: (next) => {
2856-
object_assign(props, next);
2857-
},
2858-
$destroy
2859-
});
2860-
2861-
for (const key of object_keys(accessors || {})) {
2862-
define_property(result, key, {
2863-
get() {
2864-
// @ts-expect-error TS doesn't know key exists on accessor
2865-
return accessors[key];
2866-
},
2867-
/** @param {any} value */
2868-
set(value) {
2869-
// @ts-expect-error TS doesn't know key exists on accessor
2870-
flushSync(() => (accessors[key] = value));
2871-
},
2872-
enumerable: true
2873-
});
2874-
}
2875-
2876-
return result;
2854+
export function mount(component, options) {
2855+
init_operations();
2856+
const anchor = empty();
2857+
options.target.appendChild(anchor);
2858+
// Don't flush previous effects to ensure order of outer effects stays consistent
2859+
return flush_sync(() => _mount(component, { ...options, anchor }), false);
28772860
}
28782861

28792862
/**
2880-
* Mounts the given component to the given target and returns the accessors of the component and a function to destroy it.
2881-
*
2882-
* If you need to interact with the component after mounting, use `createRoot` instead.
2863+
* Hydrates a component on the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component
28832864
*
28842865
* @template {Record<string, any>} Props
2885-
* @template {Record<string, any> | undefined} Exports
2866+
* @template {Record<string, any>} Exports
28862867
* @template {Record<string, any>} Events
28872868
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
28882869
* @param {{
@@ -2891,19 +2872,65 @@ export function createRoot(component, options) {
28912872
* events?: Events;
28922873
* context?: Map<any, any>;
28932874
* intro?: boolean;
2875+
* recover?: false;
28942876
* }} options
2895-
* @returns {[Exports, () => void]}
2877+
* @returns {Exports}
28962878
*/
2897-
export function mount(component, options) {
2879+
export function hydrate(component, options) {
28982880
init_operations();
2899-
const anchor = empty();
2900-
options.target.appendChild(anchor);
2901-
return _mount(component, { ...options, anchor });
2881+
const container = options.target;
2882+
const first_child = /** @type {ChildNode} */ (container.firstChild);
2883+
// Call with insert_text == true to prevent empty {expressions} resulting in an empty
2884+
// fragment array, resulting in a hydration error down the line
2885+
const hydration_fragment = get_hydration_fragment(first_child, true);
2886+
const previous_hydration_fragment = current_hydration_fragment;
2887+
set_current_hydration_fragment(hydration_fragment);
2888+
2889+
/** @type {null | Text} */
2890+
let anchor = null;
2891+
if (hydration_fragment === null) {
2892+
anchor = empty();
2893+
container.appendChild(anchor);
2894+
}
2895+
2896+
let finished_hydrating = false;
2897+
2898+
try {
2899+
// Don't flush previous effects to ensure order of outer effects stays consistent
2900+
return flush_sync(() => {
2901+
const instance = _mount(component, { ...options, anchor });
2902+
// flush_sync will run this callback and then synchronously run any pending effects,
2903+
// which don't belong to the hydration phase anymore - therefore reset it here
2904+
set_current_hydration_fragment(null);
2905+
finished_hydrating = true;
2906+
return instance;
2907+
}, false);
2908+
} catch (error) {
2909+
if (!finished_hydrating && options.recover !== false && hydration_fragment !== null) {
2910+
// eslint-disable-next-line no-console
2911+
console.error(
2912+
'ERR_SVELTE_HYDRATION_MISMATCH' +
2913+
(DEV
2914+
? ': Hydration failed because the initial UI does not match what was rendered on the server.'
2915+
: ''),
2916+
error
2917+
);
2918+
remove(hydration_fragment);
2919+
first_child.remove();
2920+
hydration_fragment.at(-1)?.nextSibling?.remove();
2921+
set_current_hydration_fragment(null);
2922+
return mount(component, options);
2923+
} else {
2924+
throw error;
2925+
}
2926+
} finally {
2927+
set_current_hydration_fragment(previous_hydration_fragment);
2928+
}
29022929
}
29032930

29042931
/**
29052932
* @template {Record<string, any>} Props
2906-
* @template {Record<string, any> | undefined} Exports
2933+
* @template {Record<string, any>} Exports
29072934
* @template {Record<string, any>} Events
29082935
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
29092936
* @param {{
@@ -2915,7 +2942,7 @@ export function mount(component, options) {
29152942
* intro?: boolean;
29162943
* recover?: false;
29172944
* }} options
2918-
* @returns {[Exports, () => void]}
2945+
* @returns {Exports}
29192946
*/
29202947
function _mount(component, options) {
29212948
const registered_events = new Set();
@@ -2934,7 +2961,7 @@ function _mount(component, options) {
29342961
options.context;
29352962
}
29362963
// @ts-expect-error the public typings are not what the actual function looks like
2937-
accessors = component(options.anchor, options.props || {});
2964+
accessors = component(options.anchor, options.props || {}) || {};
29382965
if (options.context) {
29392966
pop();
29402967
}
@@ -2981,80 +3008,38 @@ function _mount(component, options) {
29813008
event_handle(array_from(all_registerd_events));
29823009
root_event_handles.add(event_handle);
29833010

2984-
return [
2985-
accessors,
2986-
() => {
2987-
for (const event_name of registered_events) {
2988-
container.removeEventListener(event_name, bound_event_listener);
2989-
}
2990-
root_event_handles.delete(event_handle);
2991-
const dom = block.d;
2992-
if (dom !== null) {
2993-
remove(dom);
2994-
}
2995-
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
3011+
mounted_components.set(accessors, () => {
3012+
for (const event_name of registered_events) {
3013+
container.removeEventListener(event_name, bound_event_listener);
3014+
}
3015+
root_event_handles.delete(event_handle);
3016+
const dom = block.d;
3017+
if (dom !== null) {
3018+
remove(dom);
29963019
}
2997-
];
3020+
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
3021+
});
3022+
3023+
return accessors;
29983024
}
29993025

30003026
/**
3001-
* Hydrates the given component to the given target and returns the accessors of the component and a function to destroy it.
3002-
*
3003-
* If you need to interact with the component after hydrating, use `createRoot` instead.
3004-
*
3005-
* @template {Record<string, any>} Props
3006-
* @template {Record<string, any> | undefined} Exports
3007-
* @template {Record<string, any>} Events
3008-
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
3009-
* @param {{
3010-
* target: Node;
3011-
* props?: Props;
3012-
* events?: Events;
3013-
* context?: Map<any, any>;
3014-
* intro?: boolean;
3015-
* recover?: false;
3016-
* }} options
3017-
* @returns {[Exports, () => void]}
3027+
* References of the accessors of all components that were `mount`ed or `hydrate`d.
3028+
* Uses a `WeakMap` to avoid memory leaks.
30183029
*/
3019-
export function hydrate(component, options) {
3020-
init_operations();
3021-
const container = options.target;
3022-
const first_child = /** @type {ChildNode} */ (container.firstChild);
3023-
// Call with insert_text == true to prevent empty {expressions} resulting in an empty
3024-
// fragment array, resulting in a hydration error down the line
3025-
const hydration_fragment = get_hydration_fragment(first_child, true);
3026-
const previous_hydration_fragment = current_hydration_fragment;
3030+
let mounted_components = new WeakMap();
30273031

3028-
try {
3029-
/** @type {null | Text} */
3030-
let anchor = null;
3031-
if (hydration_fragment === null) {
3032-
anchor = empty();
3033-
container.appendChild(anchor);
3034-
}
3035-
set_current_hydration_fragment(hydration_fragment);
3036-
return _mount(component, { ...options, anchor });
3037-
} catch (error) {
3038-
if (options.recover !== false && hydration_fragment !== null) {
3039-
// eslint-disable-next-line no-console
3040-
console.error(
3041-
'ERR_SVELTE_HYDRATION_MISMATCH' +
3042-
(DEV
3043-
? ': Hydration failed because the initial UI does not match what was rendered on the server.'
3044-
: ''),
3045-
error
3046-
);
3047-
remove(hydration_fragment);
3048-
first_child.remove();
3049-
hydration_fragment.at(-1)?.nextSibling?.remove();
3050-
set_current_hydration_fragment(null);
3051-
return mount(component, options);
3052-
} else {
3053-
throw error;
3054-
}
3055-
} finally {
3056-
set_current_hydration_fragment(previous_hydration_fragment);
3032+
/**
3033+
* Unmounts a component that was previously mounted using `mount` or `hydrate`.
3034+
* @param {Record<string, any>} component
3035+
*/
3036+
export function unmount(component) {
3037+
const destroy = mounted_components.get(component);
3038+
if (DEV && !destroy) {
3039+
// eslint-disable-next-line no-console
3040+
console.warn('Tried to unmount a component that was not mounted.');
30573041
}
3042+
destroy?.();
30583043
}
30593044

30603045
/**

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

+21-3
Original file line numberDiff line numberDiff line change
@@ -749,9 +749,22 @@ export function flush_local_pre_effects(context) {
749749
* @returns {void}
750750
*/
751751
export function flushSync(fn) {
752+
flush_sync(fn);
753+
}
754+
755+
/**
756+
* Internal version of `flushSync` with the option to not flush previous effects.
757+
* Returns the result of the passed function, if given.
758+
* @param {() => any} [fn]
759+
* @param {boolean} [flush_previous]
760+
* @returns {any}
761+
*/
762+
export function flush_sync(fn, flush_previous = true) {
752763
const previous_scheduler_mode = current_scheduler_mode;
753764
const previous_queued_pre_and_render_effects = current_queued_pre_and_render_effects;
754765
const previous_queued_effects = current_queued_effects;
766+
let result;
767+
755768
try {
756769
infinite_loop_guard();
757770
/** @type {import('./types.js').EffectSignal[]} */
@@ -762,10 +775,12 @@ export function flushSync(fn) {
762775
current_scheduler_mode = FLUSH_SYNC;
763776
current_queued_pre_and_render_effects = pre_and_render_effects;
764777
current_queued_effects = effects;
765-
flush_queued_effects(previous_queued_pre_and_render_effects);
766-
flush_queued_effects(previous_queued_effects);
778+
if (flush_previous) {
779+
flush_queued_effects(previous_queued_pre_and_render_effects);
780+
flush_queued_effects(previous_queued_effects);
781+
}
767782
if (fn !== undefined) {
768-
fn();
783+
result = fn();
769784
}
770785
if (current_queued_pre_and_render_effects.length > 0 || effects.length > 0) {
771786
flushSync();
@@ -779,9 +794,12 @@ export function flushSync(fn) {
779794
flush_count = 0;
780795
} finally {
781796
current_scheduler_mode = previous_scheduler_mode;
797+
// TODO is this correct to reset the previous queues? They're flushed if flush_previous is true so should we set it to empty in that case instead?
782798
current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects;
783799
current_queued_effects = previous_queued_effects;
784800
}
801+
802+
return result;
785803
}
786804

787805
/**

0 commit comments

Comments
 (0)