fix: resolve legacy component props equality for mutations (#12348)

* fix: resolve legacy component props equality for mutations

* lint

* Update packages/svelte/src/legacy/legacy-client.js

* simplify test

* rename test

* make all proxies immutable

* remove unused parameter

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12353/head
Dominic Gannaway 4 months ago committed by GitHub
parent 63f3ee4ffd
commit ee47696986
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: resolve legacy component props equality for mutations

@ -454,7 +454,6 @@ export function serialize_proxy_reassignment(value, proxy_reference, state) {
? b.call( ? b.call(
'$.proxy', '$.proxy',
value, value,
b.true,
b.null, b.null,
typeof proxy_reference === 'string' typeof proxy_reference === 'string'
? b.id(proxy_reference) ? b.id(proxy_reference)

@ -11,7 +11,7 @@ import {
object_prototype object_prototype
} from './utils.js'; } from './utils.js';
import { check_ownership, widen_ownership } from './dev/ownership.js'; import { check_ownership, widen_ownership } from './dev/ownership.js';
import { mutable_source, source, set } from './reactivity/sources.js'; import { source, set } from './reactivity/sources.js';
import { STATE_FROZEN_SYMBOL, STATE_SYMBOL } from './constants.js'; import { STATE_FROZEN_SYMBOL, STATE_SYMBOL } from './constants.js';
import { UNINITIALIZED } from '../../constants.js'; import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js'; import * as e from './errors.js';
@ -19,12 +19,11 @@ import * as e from './errors.js';
/** /**
* @template T * @template T
* @param {T} value * @param {T} value
* @param {boolean} [immutable]
* @param {import('#client').ProxyMetadata | null} [parent] * @param {import('#client').ProxyMetadata | null} [parent]
* @param {import('#client').Source<T>} [prev] dev mode only * @param {import('#client').Source<T>} [prev] dev mode only
* @returns {import('#client').ProxyStateObject<T> | T} * @returns {import('#client').ProxyStateObject<T> | T}
*/ */
export function proxy(value, immutable = true, parent = null, prev) { export function proxy(value, parent = null, prev) {
if ( if (
typeof value === 'object' && typeof value === 'object' &&
value != null && value != null &&
@ -59,7 +58,6 @@ export function proxy(value, immutable = true, parent = null, prev) {
s: new Map(), s: new Map(),
v: source(0), v: source(0),
a: is_array(value), a: is_array(value),
i: immutable,
p: proxy, p: proxy,
t: value t: value
}), }),
@ -169,7 +167,7 @@ const state_proxy_handler = {
const metadata = target[STATE_SYMBOL]; const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop); const s = metadata.s.get(prop);
if (s !== undefined) set(s, proxy(descriptor.value, metadata.i, metadata)); if (s !== undefined) set(s, proxy(descriptor.value, metadata));
} }
return Reflect.defineProperty(target, prop, descriptor); return Reflect.defineProperty(target, prop, descriptor);
@ -215,7 +213,7 @@ const state_proxy_handler = {
// create a source, but only if it's an own property and not a prototype property // create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!(prop in target) || get_descriptor(target, prop)?.writable)) { if (s === undefined && (!(prop in target) || get_descriptor(target, prop)?.writable)) {
s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i, metadata)); s = source(proxy(target[prop], metadata));
metadata.s.set(prop, s); metadata.s.set(prop, s);
} }
@ -256,9 +254,7 @@ const state_proxy_handler = {
(current_effect !== null && (!has || get_descriptor(target, prop)?.writable)) (current_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) { ) {
if (s === undefined) { if (s === undefined) {
s = (metadata.i ? source : mutable_source)( s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED);
has ? proxy(target[prop], metadata.i, metadata) : UNINITIALIZED
);
metadata.s.set(prop, s); metadata.s.set(prop, s);
} }
const value = get(s); const value = get(s);
@ -283,7 +279,7 @@ const state_proxy_handler = {
s = metadata.s.get(prop); s = metadata.s.get(prop);
} }
if (s !== undefined) { if (s !== undefined) {
set(s, proxy(value, metadata.i, metadata)); set(s, proxy(value, metadata));
} }
const is_array = metadata.a; const is_array = metadata.a;
const not_has = !(prop in target); const not_has = !(prop in target);

@ -180,8 +180,6 @@ export interface ProxyMetadata<T = Record<string | symbol, any>> {
v: Source<number>; v: Source<number>;
/** `true` if the proxified object is an array */ /** `true` if the proxified object is an array */
a: boolean; a: boolean;
/** Immutable: Whether to use a source or mutable source under the hood */
i: boolean;
/** The associated proxy */ /** The associated proxy */
p: ProxyStateObject<T>; p: ProxyStateObject<T>;
/** The original target this proxy was created for */ /** The original target this proxy was created for */

@ -1,8 +1,9 @@
/** @import { ComponentConstructorOptions, ComponentType, SvelteComponent, Component } from 'svelte' */ /** @import { ComponentConstructorOptions, ComponentType, SvelteComponent, Component } from 'svelte' */
import { proxy } from '../internal/client/proxy.js'; import { mutable_source, get, set } from 'svelte/internal/client';
import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { hydrate, mount, unmount } from '../internal/client/render.js'; import { hydrate, mount, unmount } from '../internal/client/render.js';
import { define_property } from '../internal/client/utils.js'; import { define_property } from '../internal/client/utils.js';
import { safe_not_equal } from '../internal/client/reactivity/equality.js';
/** /**
* Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component. * Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component.
@ -69,10 +70,51 @@ class Svelte4Component {
* }} options * }} options
*/ */
constructor(options) { constructor(options) {
// Using proxy state here isn't completely mirroring the Svelte 4 behavior, because mutations to a property var sources = new Map();
// cause fine-grained updates to only the places where that property is used, and not the entire property. var add_source = (/** @type {string | symbol} */ key) => {
// Reactive statements and actions (the things where this matters) are handling this properly regardless, so it should be fine in practise. var s = mutable_source(0);
const props = proxy({ ...(options.props || {}), $$events: {} }, false); sources.set(key, s);
return s;
};
// Replicate coarse-grained props through a proxy that has a version source for
// each property, which is increment on updates to the property itself. Do not
// use our $state proxy because that one has fine-grained reactivity.
const props = new Proxy(
{ ...(options.props || {}), $$events: {} },
{
get(target, prop, receiver) {
var value = Reflect.get(target, prop, receiver);
var s = sources.get(prop);
if (s === undefined) {
s = add_source(prop);
}
get(s);
return value;
},
has(target, prop) {
var value = Reflect.has(target, prop);
var s = sources.get(prop);
if (s !== undefined) {
get(s);
}
return value;
},
set(target, prop, value) {
var s = sources.get(prop);
// @ts-ignore
var prev_value = target[prop];
if (s === undefined) {
s = add_source(prop);
} else if (safe_not_equal(prev_value, value)) {
// Increment version
set(s, s.v + 1);
}
// @ts-ignore
target[prop] = value;
return true;
}
}
);
this.#instance = (options.hydrate ? hydrate : mount)(options.component, { this.#instance = (options.hydrate ? hydrate : mount)(options.component, {
target: options.target, target: options.target,
props, props,

@ -0,0 +1,24 @@
import { test } from '../../test';
const data = {
message: 'hello'
};
export default test({
get props() {
data.message = 'hello';
return {
data
};
},
html: '<p>hello</p>',
async test({ assert, component, target }) {
data.message = 'goodbye';
await component.$set({ data });
assert.htmlEqual(target.innerHTML, '<p>goodbye</p>');
}
});

@ -0,0 +1,5 @@
<script>
export let data;
</script>
<p>{data.message}</p>
Loading…
Cancel
Save