diff --git a/.changeset/gorgeous-jokes-sit.md b/.changeset/gorgeous-jokes-sit.md new file mode 100644 index 000000000000..934bae780fce --- /dev/null +++ b/.changeset/gorgeous-jokes-sit.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +breaking: array proxy coercion is no longer reactive in template diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 53df86126ac4..dd9fc2a5249e 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -3,22 +3,23 @@ export const EFFECT = 1 << 2; export const RENDER_EFFECT = 1 << 3; export const BLOCK_EFFECT = 1 << 4; export const BRANCH_EFFECT = 1 << 5; -export const ROOT_EFFECT = 1 << 6; -export const UNOWNED = 1 << 7; -export const DISCONNECTED = 1 << 8; -export const CLEAN = 1 << 9; -export const DIRTY = 1 << 10; -export const MAYBE_DIRTY = 1 << 11; -export const INERT = 1 << 12; -export const DESTROYED = 1 << 13; -export const EFFECT_RAN = 1 << 14; +export const TEMPLATE_EFFECT = 1 << 6; +export const ROOT_EFFECT = 1 << 7; +export const UNOWNED = 1 << 8; +export const DISCONNECTED = 1 << 9; +export const CLEAN = 1 << 10; +export const DIRTY = 1 << 11; +export const MAYBE_DIRTY = 1 << 12; +export const INERT = 1 << 13; +export const DESTROYED = 1 << 14; +export const EFFECT_RAN = 1 << 15; /** 'Transparent' effects do not create a transition boundary */ -export const EFFECT_TRANSPARENT = 1 << 15; +export const EFFECT_TRANSPARENT = 1 << 16; /** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ -export const LEGACY_DERIVED_PROP = 1 << 16; -export const INSPECT_EFFECT = 1 << 17; -export const HEAD_EFFECT = 1 << 18; -export const EFFECT_HAS_DERIVED = 1 << 19; +export const LEGACY_DERIVED_PROP = 1 << 17; +export const INSPECT_EFFECT = 1 << 18; +export const HEAD_EFFECT = 1 << 19; +export const EFFECT_HAS_DERIVED = 1 << 20; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 7454a22183ac..3409b013cc5f 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,6 +1,6 @@ /** @import { ProxyMetadata, ProxyStateObject, Source } from '#client' */ import { DEV } from 'esm-env'; -import { get, component_context, active_effect } from './runtime.js'; +import { get, component_context, active_effect, untrack, active_reaction } from './runtime.js'; import { array_prototype, get_descriptor, @@ -10,7 +10,7 @@ import { } from '../shared/utils.js'; import { check_ownership, widen_ownership } from './dev/ownership.js'; import { source, set } from './reactivity/sources.js'; -import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js'; +import { STATE_SYMBOL, STATE_SYMBOL_METADATA, TEMPLATE_EFFECT } from './constants.js'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; @@ -115,6 +115,17 @@ export function proxy(value, parent = null, prev) { if (DEV && prop === STATE_SYMBOL_METADATA) { return metadata; } + // We untrack Symbol.toPrimitive when used within a template effect. If people want explicit reactivity, + // they should use toString() or some other coercion method instead + if ( + is_proxied_array && + prop === Symbol.toPrimitive && + active_reaction !== null && + (active_reaction.f & TEMPLATE_EFFECT) !== 0 + ) { + return (/** @type {'string' | 'number' | 'default'} */ hint) => + untrack(() => (hint === 'number' ? Number(target) : String(target))); + } if (prop === STATE_SYMBOL) { return value; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index b5955e9b32c0..41dd61c18070 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -35,7 +35,8 @@ import { INSPECT_EFFECT, HEAD_EFFECT, MAYBE_DIRTY, - EFFECT_HAS_DERIVED + EFFECT_HAS_DERIVED, + TEMPLATE_EFFECT } from '../constants.js'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -324,7 +325,7 @@ export function template_effect(fn) { value: '{expression}' }); } - return render_effect(fn); + return create_effect(RENDER_EFFECT | TEMPLATE_EFFECT, fn, true); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte index 1071d37c39c6..14e9e7acc75f 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/main.svelte @@ -18,4 +18,4 @@ -{state.data.list} +{state.data.list.toString()} diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte index fe2ac37bd3f6..ce2a5e5e2dbf 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte @@ -26,4 +26,4 @@

props: {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7}

-

log: {log}

+

log: {log.toString()}

diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte index fe2ac37bd3f6..ce2a5e5e2dbf 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy/sub.svelte @@ -26,4 +26,4 @@

props: {p0} {p1} {p2} {p3} {p4} {p5} {p6} {p7}

-

log: {log}

+

log: {log.toString()}

diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js new file mode 100644 index 000000000000..684e5870c19d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/_config.js @@ -0,0 +1,30 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + `, + + ssrHtml: ` + + `, + + test({ assert, target }) { + const [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + ` + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte new file mode 100644 index 000000000000..a02ab881c6e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-to-primitive/main.svelte @@ -0,0 +1,21 @@ + + + + +