fix: don't turn component instances stored in $state into state proxies (#18646)

Fixes #18416.

We previously said that we don't want to handle component instances specifically when they're wrapped with state in  #16747 - though the use case presented back then was much more arcane than the one in #18416. Therefore we now don't proxify component instances anymore, which also makes the dev time proxy warning obsolete.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/14594/merge
Khaled Waleed 4 days ago committed by GitHub
parent 09a67efcfc
commit 2d2e5df26e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't turn component instances stored in `$state` into state proxies

@ -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
```

@ -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 `<svelte:boundary>` `reset` function only resets the boundary the first time it is called

@ -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');

@ -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} */

@ -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);

@ -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;
}

@ -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();

@ -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 `<svelte:boundary>` `reset` function only resets the boundary the first time it is called
*/

@ -0,0 +1,7 @@
<script>
import { items } from './data.js';
export const myArr = items;
</script>
<p>child</p>

@ -0,0 +1,15 @@
import { test } from '../../test';
import { items } from './data.js';
export default test({
compileOptions: {
dev: true
},
html: `<p>child</p>`,
test({ assert, instance }) {
// ensure component instance doesn't get proxified (https://github.com/sveltejs/svelte/issues/18416)
assert.ok(instance.get_first().myArr === items);
}
});

@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
const components = $state({});
export function get_first() {
return components.first;
}
</script>
<Child bind:this={components.first} />

@ -0,0 +1,7 @@
<script>
import { items } from './data.js';
export const myArr = items;
</script>
<p>child</p>

@ -0,0 +1,11 @@
import { test } from '../../test';
import { items } from './data.js';
export default test({
html: `<p>child</p>`,
test({ assert, instance }) {
// ensure component instance doesn't get proxified (https://github.com/sveltejs/svelte/issues/18416)
assert.ok(instance.get_first().myArr === items);
}
});

@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
const components = $state({});
export function get_first() {
return components.first;
}
</script>
<Child bind:this={components.first} />
Loading…
Cancel
Save