Skip to content

Commit 316a341

Browse files
committed
fix: make it work properly with reassigned references
1 parent f16e445 commit 316a341

File tree

3 files changed

+99
-24
lines changed

3 files changed

+99
-24
lines changed

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

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
/** @import { ProxyMetadata, ProxyStateObject, Source, ValueOptions } from '#client' */
22
import { DEV } from 'esm-env';
3-
import { get, component_context, active_effect } from './runtime.js';
3+
import { UNINITIALIZED } from '../../constants.js';
4+
import { tracing_mode_flag } from '../flags/index.js';
45
import {
56
array_prototype,
67
get_descriptor,
78
get_prototype_of,
89
is_array,
910
object_prototype
1011
} from '../shared/utils.js';
11-
import { check_ownership, widen_ownership } from './dev/ownership.js';
12-
import { source, set, state, batch_onchange } from './reactivity/sources.js';
1312
import { PROXY_ONCHANGE_SYMBOL, STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js';
14-
import { UNINITIALIZED } from '../../constants.js';
15-
import * as e from './errors.js';
13+
import { check_ownership, widen_ownership } from './dev/ownership.js';
1614
import { get_stack } from './dev/tracing.js';
17-
import { tracing_mode_flag } from '../flags/index.js';
15+
import * as e from './errors.js';
16+
import { batch_onchange, set, source, state } from './reactivity/sources.js';
17+
import { active_effect, component_context, get } from './runtime.js';
1818

1919
const array_methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'];
2020

21+
/**
22+
* @param {ValueOptions | undefined} options
23+
* @returns {ValueOptions | undefined}
24+
*/
25+
function clone_options(options) {
26+
return options != null
27+
? {
28+
onchange: options.onchange
29+
}
30+
: undefined;
31+
}
32+
2133
/**
2234
* @template T
2335
* @param {T} value
@@ -39,10 +51,21 @@ export function proxy(value, options, parent = null, prev) {
3951

4052
if (STATE_SYMBOL in value) {
4153
// @ts-ignore
42-
value[PROXY_ONCHANGE_SYMBOL] = options?.onchange;
54+
value[PROXY_ONCHANGE_SYMBOL](options?.onchange);
4355
return value;
4456
}
4557

58+
if (options?.onchange) {
59+
// if there's an onchange we actually store that but override the value
60+
// to store every other onchange that new proxies might add
61+
var onchanges = new Set([options.onchange]);
62+
options.onchange = () => {
63+
for (let onchange of onchanges) {
64+
onchange();
65+
}
66+
};
67+
}
68+
4669
const prototype = get_prototype_of(value);
4770

4871
if (prototype !== object_prototype && prototype !== array_prototype) {
@@ -103,7 +126,7 @@ export function proxy(value, options, parent = null, prev) {
103126
var s = sources.get(prop);
104127

105128
if (s === undefined) {
106-
s = source(descriptor.value, options, stack);
129+
s = source(descriptor.value, clone_options(options), stack);
107130
sources.set(prop, s);
108131
} else {
109132
set(s, proxy(descriptor.value, options, metadata));
@@ -117,7 +140,7 @@ export function proxy(value, options, parent = null, prev) {
117140

118141
if (s === undefined) {
119142
if (prop in target) {
120-
sources.set(prop, source(UNINITIALIZED, options, stack));
143+
sources.set(prop, source(UNINITIALIZED, clone_options(options), stack));
121144
}
122145
} else {
123146
// When working with arrays, we need to also ensure we update the length when removing
@@ -130,6 +153,11 @@ export function proxy(value, options, parent = null, prev) {
130153
set(ls, n);
131154
}
132155
}
156+
// when we delete a property if the source is a proxy we remove the current onchange from
157+
// the proxy `onchanges` so that it doesn't trigger it anymore
158+
if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
159+
s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true);
160+
}
133161
set(s, UNINITIALIZED);
134162
update_version(version);
135163
}
@@ -146,12 +174,38 @@ export function proxy(value, options, parent = null, prev) {
146174
return value;
147175
}
148176

177+
if (prop === PROXY_ONCHANGE_SYMBOL) {
178+
return (
179+
/** @type {(() => unknown) | undefined} */ value,
180+
/** @type {boolean} */ remove
181+
) => {
182+
// we either add or remove the passed in value
183+
// to the onchanges array or we set every source onchange
184+
// to the passed in value (if it's undefined it will make the chain stop)
185+
if (options?.onchange != null && value && !remove) {
186+
onchanges.add(value);
187+
} else if (options?.onchange != null && value) {
188+
onchanges.delete(value);
189+
} else {
190+
options = {
191+
onchange: value
192+
};
193+
for (let [, s] of sources) {
194+
if (s.o) {
195+
s.o.onchange = value;
196+
}
197+
}
198+
}
199+
};
200+
}
201+
149202
var s = sources.get(prop);
150203
var exists = prop in target;
151204

152205
// create a source, but only if it's an own property and not a prototype property
153206
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
154-
s = source(proxy(exists ? target[prop] : UNINITIALIZED, options, metadata), options, stack);
207+
let opt = clone_options(options);
208+
s = source(proxy(exists ? target[prop] : UNINITIALIZED, opt, metadata), opt, stack);
155209
sources.set(prop, s);
156210
}
157211

@@ -229,7 +283,8 @@ export function proxy(value, options, parent = null, prev) {
229283
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
230284
) {
231285
if (s === undefined) {
232-
s = source(has ? proxy(target[prop], options, metadata) : UNINITIALIZED, options, stack);
286+
let opt = clone_options(options);
287+
s = source(has ? proxy(target[prop], opt, metadata) : UNINITIALIZED, opt, stack);
233288
sources.set(prop, s);
234289
}
235290

@@ -243,14 +298,6 @@ export function proxy(value, options, parent = null, prev) {
243298
},
244299

245300
set(target, prop, value, receiver) {
246-
if (prop === PROXY_ONCHANGE_SYMBOL && options?.onchange != null) {
247-
const old_onchange = options.onchange;
248-
options.onchange = () => {
249-
old_onchange();
250-
value();
251-
};
252-
return true;
253-
}
254301
var s = sources.get(prop);
255302
var has = prop in target;
256303

@@ -264,7 +311,7 @@ export function proxy(value, options, parent = null, prev) {
264311
// If the item exists in the original, we need to create a uninitialized source,
265312
// else a later read of the property would result in a source being created with
266313
// the value of the original item at that index.
267-
other_s = source(UNINITIALIZED, options, stack);
314+
other_s = source(UNINITIALIZED, clone_options(options), stack);
268315
sources.set(i + '', other_s);
269316
}
270317
}
@@ -276,13 +323,19 @@ export function proxy(value, options, parent = null, prev) {
276323
// object property before writing to that property.
277324
if (s === undefined) {
278325
if (!has || get_descriptor(target, prop)?.writable) {
279-
s = source(undefined, options, stack);
280-
set(s, proxy(value, options, metadata));
326+
const opt = clone_options(options);
327+
s = source(undefined, opt, stack);
328+
set(s, proxy(value, opt, metadata));
281329
sources.set(prop, s);
282330
}
283331
} else {
284332
has = s.v !== UNINITIALIZED;
285-
set(s, proxy(value, options, metadata));
333+
// when we set a property if the source is a proxy we remove the current onchange from
334+
// the proxy `onchanges` so that it doesn't trigger it anymore
335+
if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
336+
s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true);
337+
}
338+
set(s, proxy(value, clone_options(options), metadata));
286339
}
287340

288341
if (DEV) {

packages/svelte/tests/runtime-runes/samples/state-onchange-extrapolated-reference/_config.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { test } from '../../test';
33

44
export default test({
55
async test({ assert, target, logs }) {
6-
const [btn, btn2, btn3, btn4] = target.querySelectorAll('button');
6+
const [btn, btn2, btn3, btn4, btn5, btn6] = target.querySelectorAll('button');
77
logs.length = 0;
88

99
flushSync(() => {
@@ -18,6 +18,16 @@ export default test({
1818
flushSync(() => {
1919
btn4.click();
2020
});
21+
flushSync(() => {
22+
btn5.click();
23+
});
2124
assert.deepEqual(logs, []);
25+
flushSync(() => {
26+
btn6.click();
27+
});
28+
flushSync(() => {
29+
btn.click();
30+
});
31+
assert.deepEqual(logs, ['arr', 'arr']);
2232
}
2333
});

packages/svelte/tests/runtime-runes/samples/state-onchange-extrapolated-reference/main.svelte

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,21 @@
2323
const values = [...Object.values(obj)];
2424
2525
delete obj.key;
26+
27+
let arr_2 = $state([{ count: 1 }, { count: 1 }], {
28+
onchange(){
29+
console.log("arr_2");
30+
}
31+
});
32+
33+
const item_4 = arr_2[0];
34+
35+
arr_2.length = 0;
2636
</script>
2737

2838
<button onclick={()=> item.count++}>{item.count}</button>
2939
<button onclick={()=> item_2.count++}>{item_2.count}</button>
3040
<button onclick={()=> item_3.count++}>{item_3.count}</button>
3141
<button onclick={()=> values[0].count++}>{values[0].count}</button>
42+
<button onclick={()=> item_4.count++}>{item_4.count}</button>
43+
<button onclick={()=> arr.push(item)}>push</button>

0 commit comments

Comments
 (0)