diff --git a/.changeset/smart-parents-swim.md b/.changeset/smart-parents-swim.md new file mode 100644 index 0000000000..c85000aaac --- /dev/null +++ b/.changeset/smart-parents-swim.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reuse existing proxy when object has multiple references diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 8d7d9a5906..bd43aca398 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -409,10 +409,10 @@ export function analyze_component(root, options) { analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); } - // warn on any nonstate declarations that are a) reassigned and mutated and b) referenced in the template + // warn on any nonstate declarations that are a) reassigned and b) referenced in the template for (const scope of [module.scope, instance.scope]) { outer: for (const [name, binding] of scope.declarations) { - if (binding.kind === 'normal' && binding.reassigned && binding.mutated) { + if (binding.kind === 'normal' && binding.reassigned) { for (const { path } of binding.references) { if (path[0].type !== 'Fragment') continue; for (let i = 1; i < path.length; i += 1) { diff --git a/packages/svelte/src/internal/client/proxy/proxy.js b/packages/svelte/src/internal/client/proxy/proxy.js index ac94c0b492..9a68f23ec4 100644 --- a/packages/svelte/src/internal/client/proxy/proxy.js +++ b/packages/svelte/src/internal/client/proxy/proxy.js @@ -17,7 +17,7 @@ import { object_keys } from '../utils.js'; -/** @typedef {{ s: Map>; v: import('../types.js').SourceSignal; a: boolean, i: boolean }} Metadata */ +/** @typedef {{ s: Map>; v: import('../types.js').SourceSignal; a: boolean, i: boolean, p: StateObject }} Metadata */ /** @typedef {Record & { [STATE_SYMBOL]: Metadata }} StateObject */ export const STATE_SYMBOL = Symbol('$state'); @@ -35,15 +35,23 @@ const is_frozen = Object.isFrozen; * @returns {T} */ export function proxy(value, immutable = true) { - if (typeof value === 'object' && value != null && !is_frozen(value) && !(STATE_SYMBOL in value)) { + if (typeof value === 'object' && value != null && !is_frozen(value)) { + if (STATE_SYMBOL in value) { + return /** @type {T} */ (value[STATE_SYMBOL].p); + } + const prototype = get_prototype_of(value); // TODO handle Map and Set as well if (prototype === object_prototype || prototype === array_prototype) { - define_property(value, STATE_SYMBOL, { value: init(value, immutable), writable: false }); + const proxy = new Proxy(value, handler); + define_property(value, STATE_SYMBOL, { + value: init(value, proxy, immutable), + writable: false + }); // @ts-expect-error not sure how to fix this - return new Proxy(value, handler); + return proxy; } } @@ -102,15 +110,17 @@ export function unstate(value) { /** * @param {StateObject} value + * @param {StateObject} proxy * @param {boolean} immutable * @returns {Metadata} */ -function init(value, immutable) { +function init(value, proxy, immutable) { return { s: new Map(), v: source(0), a: is_array(value), - i: immutable + i: immutable, + p: proxy }; } diff --git a/packages/svelte/src/internal/client/proxy/readonly.js b/packages/svelte/src/internal/client/proxy/readonly.js index e6dc2f3828..3fce783397 100644 --- a/packages/svelte/src/internal/client/proxy/readonly.js +++ b/packages/svelte/src/internal/client/proxy/readonly.js @@ -42,7 +42,7 @@ export function readonly(value) { */ const readonly_error = (_, prop) => { throw new Error( - `Props cannot be mutated, unless used with \`bind:\`. Use \`bind:prop-in-question={..}\` to make \`${prop}\` settable. Fallback values can never be mutated.` + `Non-bound props cannot be mutated — use \`bind:={...}\` to make \`${prop}\` settable. Fallback values can never be mutated.` ); }; diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 13af5dcf66..c4161018dc 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -377,10 +377,7 @@ export function class_name(dom, value) { * @returns {void} */ export function text_effect(dom, value) { - render_effect(() => { - const string = value(); - text(dom, string); - }); + render_effect(() => text(dom, value())); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js index 62fabeac06..352d874648 100644 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js @@ -15,5 +15,5 @@ export default test({ }, runtime_error: - 'Props cannot be mutated, unless used with `bind:`. Use `bind:prop-in-question={..}` to make `count` settable. Fallback values can never be mutated.' + 'Non-bound props cannot be mutated — use `bind:={...}` to make `count` settable. Fallback values can never be mutated.' }); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js index 62fabeac06..352d874648 100644 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js @@ -15,5 +15,5 @@ export default test({ }, runtime_error: - 'Props cannot be mutated, unless used with `bind:`. Use `bind:prop-in-question={..}` to make `count` settable. Fallback values can never be mutated.' + 'Non-bound props cannot be mutated — use `bind:={...}` to make `count` settable. Fallback values can never be mutated.' }); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-shared/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-shared/_config.js new file mode 100644 index 0000000000..d8baaf4915 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-shared/_config.js @@ -0,0 +1,30 @@ +import { test } from '../../test'; + +export default test({ + html: ` + + + `, + + async test({ assert, target }) { + const [btn1, btn2] = target.querySelectorAll('button'); + + await btn1?.click(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + await btn2?.click(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-shared/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-shared/main.svelte new file mode 100644 index 0000000000..0e0b622372 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-shared/main.svelte @@ -0,0 +1,14 @@ + + + + +