Skip to content

Commit 107ec1c

Browse files
authored
fix: handle deep assignments to $state() class properties correctly (#10289)
fixes #10276
1 parent c8da996 commit 107ec1c

File tree

4 files changed

+63
-13
lines changed

4 files changed

+63
-13
lines changed

.changeset/kind-rings-flash.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: handle deep assignments to `$state()` class properties correctly

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

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import * as b from '../../../utils/builders.js';
2-
import { extract_paths, is_simple_expression, unwrap_ts_expression } from '../../../utils/ast.js';
2+
import {
3+
extract_paths,
4+
is_simple_expression,
5+
object,
6+
unwrap_ts_expression
7+
} from '../../../utils/ast.js';
38
import { error } from '../../../errors.js';
49
import {
510
PROPS_IS_LAZY_INITIAL,
@@ -280,12 +285,13 @@ export function serialize_set_binding(node, context, fallback, options) {
280285
error(node, 'INTERNAL', `Unexpected assignment type ${assignee.type}`);
281286
}
282287

283-
let left = assignee;
284-
285288
// Handle class private/public state assignment cases
286-
while (left.type === 'MemberExpression') {
287-
if (left.object.type === 'ThisExpression' && left.property.type === 'PrivateIdentifier') {
288-
const private_state = context.state.private_state.get(left.property.name);
289+
if (assignee.type === 'MemberExpression') {
290+
if (
291+
assignee.object.type === 'ThisExpression' &&
292+
assignee.property.type === 'PrivateIdentifier'
293+
) {
294+
const private_state = context.state.private_state.get(assignee.property.name);
289295
const value = get_assignment_value(node, context);
290296
if (private_state !== undefined) {
291297
if (state.in_constructor) {
@@ -307,7 +313,7 @@ export function serialize_set_binding(node, context, fallback, options) {
307313
} else {
308314
return b.call(
309315
'$.set',
310-
left,
316+
assignee,
311317
context.state.analysis.runes &&
312318
!options?.skip_proxy_and_freeze &&
313319
should_proxy_or_freeze(value, context.state.scope)
@@ -319,11 +325,11 @@ export function serialize_set_binding(node, context, fallback, options) {
319325
}
320326
}
321327
} else if (
322-
left.object.type === 'ThisExpression' &&
323-
left.property.type === 'Identifier' &&
328+
assignee.object.type === 'ThisExpression' &&
329+
assignee.property.type === 'Identifier' &&
324330
state.in_constructor
325331
) {
326-
const public_state = context.state.public_state.get(left.property.name);
332+
const public_state = context.state.public_state.get(assignee.property.name);
327333
const value = get_assignment_value(node, context);
328334
// See if we should wrap value in $.proxy
329335
if (
@@ -342,11 +348,11 @@ export function serialize_set_binding(node, context, fallback, options) {
342348
}
343349
}
344350
}
345-
// @ts-expect-error
346-
left = unwrap_ts_expression(left.object);
347351
}
348352

349-
if (left.type !== 'Identifier') {
353+
const left = object(assignee);
354+
355+
if (left === null) {
350356
return fallback();
351357
}
352358

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<button>0 / 0</button><button>0 / 0</button>`,
5+
6+
async test({ assert, target }) {
7+
const [btn1, btn2] = target.querySelectorAll('button');
8+
9+
await btn1?.click();
10+
assert.htmlEqual(target.innerHTML, `<button>1 / 0</button><button>1 / 0</button>`);
11+
12+
await btn2?.click();
13+
assert.htmlEqual(target.innerHTML, `<button>2 / 1</button><button>2 / 1</button>`);
14+
}
15+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script>
2+
class Counter {
3+
container = $state({ count: -1 });
4+
#private = $state({ count: -1 });
5+
6+
constructor(initial_count) {
7+
this.container.count = initial_count;
8+
this.#private.count = initial_count;
9+
}
10+
11+
increment() {
12+
this.container.count += 1;
13+
this.#private.count += 1;
14+
}
15+
16+
get private_count() {
17+
return this.#private.count;
18+
}
19+
}
20+
const counter = new Counter(0);
21+
</script>
22+
23+
<button on:click={() => counter.container.count++}>{counter.container.count} / {counter.private_count}</button>
24+
<button on:click={() => counter.increment()}>{counter.container.count} / {counter.private_count}</button>

0 commit comments

Comments
 (0)