Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shaggy-spies-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: proxify values when assigning using `||=`, `&&=` and `??=` operators
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,21 @@ function build_assignment(operator, left, right, context) {

if (private_state !== undefined) {
let transformed = false;
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);

if (should_proxy(value, context.state.scope)) {
if (
private_state.kind === 'state' &&
// other operators result in coercion
['=', '||=', '&&=', '??='].includes(operator) &&
should_proxy(right, context.state.scope)
) {
transformed = true;
value =
private_state.kind === 'raw_state'
? value
: build_proxy_reassignment(value, b.member(b.this, private_state.id));
right = build_proxy_reassignment(right, b.member(b.this, private_state.id));
}

let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);

if (!context.state.in_constructor) {
return b.call('$.set', left, value);
} else if (transformed) {
Expand Down Expand Up @@ -80,10 +83,6 @@ function build_assignment(operator, left, right, context) {

// reassignment
if (object === left && transform?.assign) {
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);

// special case — if an element binding, we know it's a primitive
const path = context.path.map((node) => node.type);
const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement';
Expand All @@ -92,12 +91,19 @@ function build_assignment(operator, left, right, context) {
!is_primitive &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'raw_state' &&
context.state.analysis.runes &&
should_proxy(value, context.state.scope)
should_proxy(right, context.state.scope) &&
// other operators result in coercion
['=', '||=', '&&=', '??='].includes(operator)
) {
value = binding.kind === 'raw_state' ? value : build_proxy_reassignment(value, object);
right = build_proxy_reassignment(right, object);
}

let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);

return transform.assign(object, value);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
html: `<button>items: null</button><button>items: null</button>`,

test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');

flushSync(() => btn1.click());
assert.htmlEqual(target.innerHTML, `<button>items: [0]</button><button>items: null</button>`);

flushSync(() => btn1.click());
assert.htmlEqual(target.innerHTML, `<button>items: [0,1]</button><button>items: null</button>`);

flushSync(() => btn2.click());
assert.htmlEqual(target.innerHTML, `<button>items: [0,1]</button><button>items: [0]</button>`);

flushSync(() => btn2.click());
assert.htmlEqual(
target.innerHTML,
`<button>items: [0,1]</button><button>items: [0,1]</button>`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script>
let items = $state(null);

class Foo {
#items = $state(null);

get items() {
return this.#items;
}

add() {
(this.#items ??= []).push(this.#items.length);
}
}

const foo = new Foo();
</script>

<button onclick={() => (items ??= []).push(items.length)}>
items: {JSON.stringify(items)}
</button>

<button onclick={() => foo.add()}>
items: {JSON.stringify(foo.items)}
</button>
Loading