Skip to content

Commit

Permalink
breaking: remove createRoot, adjust mount/hydrate APIs, introdu…
Browse files Browse the repository at this point in the history
…ce `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>
  • Loading branch information
dummdidumm and Rich-Harris authored Feb 18, 2024
1 parent 11b6945 commit 2755401
Show file tree
Hide file tree
Showing 19 changed files with 253 additions and 189 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-apricots-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount`
221 changes: 103 additions & 118 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
untrack,
effect,
flushSync,
flush_sync,
safe_not_equal,
current_block,
managed_effect,
Expand All @@ -64,12 +65,11 @@ import {
get_descriptors,
is_array,
is_function,
object_assign,
object_keys
object_assign
} from './utils.js';
import { is_promise } from '../common.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>} */
const all_registerd_events = new Set();
Expand Down Expand Up @@ -2825,14 +2825,21 @@ export function spread_props(...props) {
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
* as well as a `$set` and `$destroy` method to update the props of the component or destroy it.
*
* If you don't need to interact with the component after mounting, use `mount` instead to save some bytes.
* @deprecated Use `mount` or `hydrate` instead
*/
export function createRoot() {
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> | undefined} Exports
* @template {Record<string, any>} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{
Expand All @@ -2841,48 +2848,22 @@ export function spread_props(...props) {
* events?: Events;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* }} options
* @returns {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }}
* @returns {Exports}
*/
export function createRoot(component, options) {
const props = proxy(/** @type {any} */ (options.props) || {}, false);

let [accessors, $destroy] = hydrate(component, { ...options, props });

const result =
/** @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;
export function mount(component, options) {
init_operations();
const anchor = empty();
options.target.appendChild(anchor);
// Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => _mount(component, { ...options, anchor }), false);
}

/**
* Mounts the given component to the given target and returns the accessors of the component and a function to destroy it.
*
* If you need to interact with the component after mounting, use `createRoot` instead.
* Hydrates a component on 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> | undefined} Exports
* @template {Record<string, any>} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{
Expand All @@ -2891,19 +2872,65 @@ export function createRoot(component, options) {
* events?: Events;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* }} options
* @returns {[Exports, () => void]}
* @returns {Exports}
*/
export function mount(component, options) {
export function hydrate(component, options) {
init_operations();
const anchor = empty();
options.target.appendChild(anchor);
return _mount(component, { ...options, anchor });
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;
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> | undefined} Exports
* @template {Record<string, any>} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{
Expand All @@ -2915,7 +2942,7 @@ export function mount(component, options) {
* intro?: boolean;
* recover?: false;
* }} options
* @returns {[Exports, () => void]}
* @returns {Exports}
*/
function _mount(component, options) {
const registered_events = new Set();
Expand All @@ -2934,7 +2961,7 @@ function _mount(component, options) {
options.context;
}
// @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) {
pop();
}
Expand Down Expand Up @@ -2981,80 +3008,38 @@ function _mount(component, options) {
event_handle(array_from(all_registerd_events));
root_event_handles.add(event_handle);

return [
accessors,
() => {
for (const event_name of registered_events) {
container.removeEventListener(event_name, bound_event_listener);
}
root_event_handles.delete(event_handle);
const dom = block.d;
if (dom !== null) {
remove(dom);
}
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
mounted_components.set(accessors, () => {
for (const event_name of registered_events) {
container.removeEventListener(event_name, bound_event_listener);
}
root_event_handles.delete(event_handle);
const dom = block.d;
if (dom !== null) {
remove(dom);
}
];
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.
*
* 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]}
* References of the accessors of all components that were `mount`ed or `hydrate`d.
* Uses a `WeakMap` to avoid memory leaks.
*/
export function hydrate(component, options) {
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;
let mounted_components = new WeakMap();

try {
/** @type {null | Text} */
let anchor = null;
if (hydration_fragment === null) {
anchor = empty();
container.appendChild(anchor);
}
set_current_hydration_fragment(hydration_fragment);
return _mount(component, { ...options, anchor });
} 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);
/**
* Unmounts a component that was previously mounted using `mount` or `hydrate`.
* @param {Record<string, any>} component
*/
export function unmount(component) {
const destroy = mounted_components.get(component);
if (DEV && !destroy) {
// eslint-disable-next-line no-console
console.warn('Tried to unmount a component that was not mounted.');
}
destroy?.();
}

/**
Expand Down
23 changes: 20 additions & 3 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -749,9 +749,22 @@ export function flush_local_pre_effects(context) {
* @returns {void}
*/
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_queued_pre_and_render_effects = current_queued_pre_and_render_effects;
const previous_queued_effects = current_queued_effects;
let result;

try {
infinite_loop_guard();
/** @type {import('./types.js').EffectSignal[]} */
Expand All @@ -762,10 +775,12 @@ export function flushSync(fn) {
current_scheduler_mode = FLUSH_SYNC;
current_queued_pre_and_render_effects = pre_and_render_effects;
current_queued_effects = effects;
flush_queued_effects(previous_queued_pre_and_render_effects);
flush_queued_effects(previous_queued_effects);
if (flush_previous) {
flush_queued_effects(previous_queued_pre_and_render_effects);
flush_queued_effects(previous_queued_effects);
}
if (fn !== undefined) {
fn();
result = fn();
}
if (current_queued_pre_and_render_effects.length > 0 || effects.length > 0) {
flushSync();
Expand All @@ -782,6 +797,8 @@ export function flushSync(fn) {
current_queued_pre_and_render_effects = previous_queued_pre_and_render_effects;
current_queued_effects = previous_queued_effects;
}

return result;
}

/**
Expand Down
Loading

0 comments on commit 2755401

Please sign in to comment.