Skip to content

Commit ee47696

Browse files
trueadmdummdidummRich-Harris
authored
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 <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 63f3ee4 commit ee47696

File tree

7 files changed

+87
-18
lines changed

7 files changed

+87
-18
lines changed

.changeset/six-chicken-kneel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: resolve legacy component props equality for mutations

packages/svelte/src/compiler/phases/3-transform/client/utils.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,6 @@ export function serialize_proxy_reassignment(value, proxy_reference, state) {
454454
? b.call(
455455
'$.proxy',
456456
value,
457-
b.true,
458457
b.null,
459458
typeof proxy_reference === 'string'
460459
? b.id(proxy_reference)

packages/svelte/src/internal/client/proxy.js

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,19 @@ import {
1111
object_prototype
1212
} from './utils.js';
1313
import { check_ownership, widen_ownership } from './dev/ownership.js';
14-
import { mutable_source, source, set } from './reactivity/sources.js';
14+
import { source, set } from './reactivity/sources.js';
1515
import { STATE_FROZEN_SYMBOL, STATE_SYMBOL } from './constants.js';
1616
import { UNINITIALIZED } from '../../constants.js';
1717
import * as e from './errors.js';
1818

1919
/**
2020
* @template T
2121
* @param {T} value
22-
* @param {boolean} [immutable]
2322
* @param {import('#client').ProxyMetadata | null} [parent]
2423
* @param {import('#client').Source<T>} [prev] dev mode only
2524
* @returns {import('#client').ProxyStateObject<T> | T}
2625
*/
27-
export function proxy(value, immutable = true, parent = null, prev) {
26+
export function proxy(value, parent = null, prev) {
2827
if (
2928
typeof value === 'object' &&
3029
value != null &&
@@ -59,7 +58,6 @@ export function proxy(value, immutable = true, parent = null, prev) {
5958
s: new Map(),
6059
v: source(0),
6160
a: is_array(value),
62-
i: immutable,
6361
p: proxy,
6462
t: value
6563
}),
@@ -169,7 +167,7 @@ const state_proxy_handler = {
169167
const metadata = target[STATE_SYMBOL];
170168

171169
const s = metadata.s.get(prop);
172-
if (s !== undefined) set(s, proxy(descriptor.value, metadata.i, metadata));
170+
if (s !== undefined) set(s, proxy(descriptor.value, metadata));
173171
}
174172

175173
return Reflect.defineProperty(target, prop, descriptor);
@@ -215,7 +213,7 @@ const state_proxy_handler = {
215213

216214
// create a source, but only if it's an own property and not a prototype property
217215
if (s === undefined && (!(prop in target) || get_descriptor(target, prop)?.writable)) {
218-
s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i, metadata));
216+
s = source(proxy(target[prop], metadata));
219217
metadata.s.set(prop, s);
220218
}
221219

@@ -256,9 +254,7 @@ const state_proxy_handler = {
256254
(current_effect !== null && (!has || get_descriptor(target, prop)?.writable))
257255
) {
258256
if (s === undefined) {
259-
s = (metadata.i ? source : mutable_source)(
260-
has ? proxy(target[prop], metadata.i, metadata) : UNINITIALIZED
261-
);
257+
s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED);
262258
metadata.s.set(prop, s);
263259
}
264260
const value = get(s);
@@ -283,7 +279,7 @@ const state_proxy_handler = {
283279
s = metadata.s.get(prop);
284280
}
285281
if (s !== undefined) {
286-
set(s, proxy(value, metadata.i, metadata));
282+
set(s, proxy(value, metadata));
287283
}
288284
const is_array = metadata.a;
289285
const not_has = !(prop in target);

packages/svelte/src/internal/client/types.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,6 @@ export interface ProxyMetadata<T = Record<string | symbol, any>> {
180180
v: Source<number>;
181181
/** `true` if the proxified object is an array */
182182
a: boolean;
183-
/** Immutable: Whether to use a source or mutable source under the hood */
184-
i: boolean;
185183
/** The associated proxy */
186184
p: ProxyStateObject<T>;
187185
/** The original target this proxy was created for */

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/** @import { ComponentConstructorOptions, ComponentType, SvelteComponent, Component } from 'svelte' */
2-
import { proxy } from '../internal/client/proxy.js';
2+
import { mutable_source, get, set } from 'svelte/internal/client';
33
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
44
import { hydrate, mount, unmount } from '../internal/client/render.js';
55
import { define_property } from '../internal/client/utils.js';
6+
import { safe_not_equal } from '../internal/client/reactivity/equality.js';
67

78
/**
89
* 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 {
6970
* }} options
7071
*/
7172
constructor(options) {
72-
// Using proxy state here isn't completely mirroring the Svelte 4 behavior, because mutations to a property
73-
// cause fine-grained updates to only the places where that property is used, and not the entire property.
74-
// Reactive statements and actions (the things where this matters) are handling this properly regardless, so it should be fine in practise.
75-
const props = proxy({ ...(options.props || {}), $$events: {} }, false);
73+
var sources = new Map();
74+
var add_source = (/** @type {string | symbol} */ key) => {
75+
var s = mutable_source(0);
76+
sources.set(key, s);
77+
return s;
78+
};
79+
// Replicate coarse-grained props through a proxy that has a version source for
80+
// each property, which is increment on updates to the property itself. Do not
81+
// use our $state proxy because that one has fine-grained reactivity.
82+
const props = new Proxy(
83+
{ ...(options.props || {}), $$events: {} },
84+
{
85+
get(target, prop, receiver) {
86+
var value = Reflect.get(target, prop, receiver);
87+
var s = sources.get(prop);
88+
if (s === undefined) {
89+
s = add_source(prop);
90+
}
91+
get(s);
92+
return value;
93+
},
94+
has(target, prop) {
95+
var value = Reflect.has(target, prop);
96+
var s = sources.get(prop);
97+
if (s !== undefined) {
98+
get(s);
99+
}
100+
return value;
101+
},
102+
set(target, prop, value) {
103+
var s = sources.get(prop);
104+
// @ts-ignore
105+
var prev_value = target[prop];
106+
if (s === undefined) {
107+
s = add_source(prop);
108+
} else if (safe_not_equal(prev_value, value)) {
109+
// Increment version
110+
set(s, s.v + 1);
111+
}
112+
// @ts-ignore
113+
target[prop] = value;
114+
return true;
115+
}
116+
}
117+
);
76118
this.#instance = (options.hydrate ? hydrate : mount)(options.component, {
77119
target: options.target,
78120
props,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { test } from '../../test';
2+
3+
const data = {
4+
message: 'hello'
5+
};
6+
7+
export default test({
8+
get props() {
9+
data.message = 'hello';
10+
11+
return {
12+
data
13+
};
14+
},
15+
16+
html: '<p>hello</p>',
17+
18+
async test({ assert, component, target }) {
19+
data.message = 'goodbye';
20+
await component.$set({ data });
21+
22+
assert.htmlEqual(target.innerHTML, '<p>goodbye</p>');
23+
}
24+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
export let data;
3+
</script>
4+
5+
<p>{data.message}</p>

0 commit comments

Comments
 (0)