diff --git a/.changeset/olive-crabs-refuse.md b/.changeset/olive-crabs-refuse.md new file mode 100644 index 0000000000..a928ae7f19 --- /dev/null +++ b/.changeset/olive-crabs-refuse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't turn component instances stored in `$state` into state proxies diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index 73c97c253c..75eff181f9 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -339,27 +339,6 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. -### state_proxy_unmount - -``` -Tried to unmount a state proxy, rather than a component -``` - -`unmount` was called with a state proxy: - -```js -import { mount, unmount } from 'svelte'; -import Component from './Component.svelte'; -let target = document.body; -// ---cut--- -let component = $state(mount(Component, { target })); - -// later... -unmount(component); -``` - -Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead. - ### svelte_boundary_reset_noop ``` diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index 58d00f3933..ac2e103ffe 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -295,25 +295,6 @@ To silence the warning, ensure that `value`: To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. -## state_proxy_unmount - -> Tried to unmount a state proxy, rather than a component - -`unmount` was called with a state proxy: - -```js -import { mount, unmount } from 'svelte'; -import Component from './Component.svelte'; -let target = document.body; -// ---cut--- -let component = $state(mount(Component, { target })); - -// later... -unmount(component); -``` - -Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead. - ## svelte_boundary_reset_noop > A `` `reset` function only resets the boundary the first time it is called diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 043b50b4b2..b92ea8598b 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -60,6 +60,8 @@ export const ASYNC = 1 << 22; export const ERROR_VALUE = 1 << 23; export const STATE_SYMBOL = Symbol('$state'); +/** Marks component export objects, so that `proxy(...)` leaves them untouched */ +export const COMPONENT_SYMBOL = Symbol('component'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); export const PROXY_PATH_SYMBOL = Symbol('proxy path'); diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 0a7b2cdf31..e1571862ed 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -5,7 +5,8 @@ import { active_effect, active_reaction } from './runtime.js'; import { create_user_effect } from './reactivity/effects.js'; import { async_mode_flag, legacy_mode_flag } from '../flags/index.js'; import { FILENAME } from '../../constants.js'; -import { BRANCH_EFFECT } from './constants.js'; +import { BRANCH_EFFECT, COMPONENT_SYMBOL } from './constants.js'; +import { define_property } from '../shared/utils.js'; import { create_context, get_or_init_context_map } from '../shared/context.js'; /** @type {ComponentContext | null} */ @@ -217,7 +218,16 @@ export function pop(component) { dev_current_component_function = component_context?.function ?? null; } - return component ?? /** @type {T} */ ({}); + return mark_as_component(component); +} + +/** + * Add a symbol to the object (or create one if undefined) to mark it as a component so it isn't proxified. + * @param {any} component + */ +export function mark_as_component(component = {}) { + define_property(component, COMPONENT_SYMBOL, { value: true }); + return component; } /** @returns {boolean} */ diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index 52f0c213d3..705adc99ec 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,6 +1,6 @@ /** @import { ComponentContext, Effect } from '#client' */ import { DESTROYING, STATE_SYMBOL } from '#client/constants'; -import { component_context } from '../../../context.js'; +import { component_context, mark_as_component } from '../../../context.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; import { active_effect, untrack } from '../../../runtime.js'; @@ -23,7 +23,12 @@ function is_bound_this(bound_value, element_or_component) { * returns all the parts of the each block context that are used in the expression * @returns {void} */ -export function bind_this(element_or_component = {}, update, get_value, get_parts) { +export function bind_this( + element_or_component = mark_as_component(), + update, + get_value, + get_parts +) { var component_effect = /** @type {ComponentContext} */ (component_context).r; var parent = /** @type {Effect} */ (active_effect); diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 51333e597f..91d82f8903 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -22,7 +22,7 @@ import { flush_eager_effects, set_eager_effects_deferred } from './reactivity/sources.js'; -import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; +import { COMPONENT_SYMBOL, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; import { tag } from './dev/tracing.js'; @@ -38,8 +38,13 @@ const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; * @returns {T} */ export function proxy(value) { - // if non-proxyable, or is already a proxy, return `value` - if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { + // if non-proxyable, a component instance, or already a proxy, return `value` + if ( + typeof value !== 'object' || + value === null || + STATE_SYMBOL in value || + COMPONENT_SYMBOL in value + ) { return value; } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 50832fb3ff..2abedb3a55 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -10,7 +10,7 @@ import { } from './dom/operations.js'; import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js'; import { active_effect } from './runtime.js'; -import { push, pop, component_context } from './context.js'; +import { push, pop, component_context, mark_as_component } from './context.js'; import { component_root } from './reactivity/effects.js'; import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js'; import { array_from } from '../shared/utils.js'; @@ -23,7 +23,7 @@ import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; import { is_passive_event } from '../../utils.js'; -import { COMMENT_NODE, STATE_SYMBOL, TEXT_CACHE } from './constants.js'; +import { COMMENT_NODE, TEXT_CACHE } from './constants.js'; import { boundary } from './dom/blocks/boundary.js'; /** @@ -193,7 +193,7 @@ function _mount( should_intro = intro; // @ts-expect-error the public typings are not what the actual function looks like - component = Component(anchor_node, props) || {}; + component = Component(anchor_node, props) || mark_as_component(); should_intro = true; if (hydrating) { @@ -323,11 +323,7 @@ export function unmount(component, options) { } if (DEV) { - if (STATE_SYMBOL in component) { - w.state_proxy_unmount(); - } else { - w.lifecycle_double_unmount(); - } + w.lifecycle_double_unmount(); } return Promise.resolve(); diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index f4e605ac96..ced1a8b2c3 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -247,17 +247,6 @@ export function state_proxy_equality_mismatch(operator) { } } -/** - * Tried to unmount a state proxy, rather than a component - */ -export function state_proxy_unmount() { - if (DEV) { - console.warn(`%c[svelte] state_proxy_unmount\n%cTried to unmount a state proxy, rather than a component\nhttps://svelte.dev/e/state_proxy_unmount`, bold, normal); - } else { - console.warn(`https://svelte.dev/e/state_proxy_unmount`); - } -} - /** * A `` `reset` function only resets the boundary the first time it is called */ diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/Child.svelte new file mode 100644 index 0000000000..2eb83744c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/Child.svelte @@ -0,0 +1,7 @@ + + +

child

diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/_config.js new file mode 100644 index 0000000000..daf97f7da6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; +import { items } from './data.js'; + +export default test({ + compileOptions: { + dev: true + }, + + html: `

child

`, + + test({ assert, instance }) { + // ensure component instance doesn't get proxified (https://github.com/sveltejs/svelte/issues/18416) + assert.ok(instance.get_first().myArr === items); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/data.js b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/data.js new file mode 100644 index 0000000000..06c15d09e4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/data.js @@ -0,0 +1 @@ +export const items = [{ id: 5, name: 'John' }]; diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/main.svelte new file mode 100644 index 0000000000..5f5ecddab2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/main.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/Child.svelte new file mode 100644 index 0000000000..2eb83744c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/Child.svelte @@ -0,0 +1,7 @@ + + +

child

diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/_config.js new file mode 100644 index 0000000000..e845e6932e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; +import { items } from './data.js'; + +export default test({ + html: `

child

`, + + test({ assert, instance }) { + // ensure component instance doesn't get proxified (https://github.com/sveltejs/svelte/issues/18416) + assert.ok(instance.get_first().myArr === items); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/data.js b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/data.js new file mode 100644 index 0000000000..06c15d09e4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/data.js @@ -0,0 +1 @@ +export const items = [{ id: 5, name: 'John' }]; diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/main.svelte new file mode 100644 index 0000000000..5f5ecddab2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/main.svelte @@ -0,0 +1,11 @@ + + +