Description
Describe the problem
Nullish coalescing assignment (??=) operator may be used to update fields that are either null or a non-empty array.
Such code pattern can be seen in Svelte internals, e.g. (internal/client/runtime.js):
(dependency.reactions ??= []).push(reaction);
If I try to use the same pattern, the results differ on whether I deal with a class field vs with a variable in a component.
Essentially, the code is the following:
let items = $state(null);
let counter = 0;
function addItem() {
(items ??= []).push( "" + (++counter));
}
function addItem2() {
(items ??= []);
items.push( "" + (++counter));
}
function addItem3() {
items = items ?? [];
items.push( "" + (++counter));
}
function addItem4() {
if ( ! items) {
items = [];
}
items.push( "" + (++counter));
}
function reset() {
items = null;
counter = 0;
}
</script>
<p>Items: {"" + items}</p>
<button onclick={() => addItem()}>Add</button>
The result is displayed with explicit conversion to string ("" + ...) so that I can differ a "null" value from an empty array.
For a class the code is
<script>
class Data {
items = $state(null);
counter = 0;
addItem() {
(this.items ??= []).push( "" + (++this.counter));
}
...
}
const data = new Data();
</script>
<p>Items: {"" + data.items}</p>
(Explicitly converting to a string ).
Expected behaviour:
I expect to see the following, after each click on an "Add" button:
- "null"
- "1"
- "1,2"
- "1,2,3"
Actual behaviour
-
"addItem3", "addItem4" work as expected, correctly.
-
"addItem2" works correctly for a Class, but it does not work for a Component:
I see "null", then "1", "1", "1" (the value is not updated). -
"addItem" does not work for a Class and does not work for a Component
For a Class I see: "null", "", "2", "2,3", "2,3,4", ... (the "1" is lost)
For a Component I see "null", then "1", "1", "1" (the value is not updated).
Describe the proposed solution
I think that it can be left as is and documented as a limitation of the $state rune.
Though if I look at the code, generated by Svelte 5.1.15, for the case of a Component it is:
function addItem() {
$.set(items, $.get(items) ?? []).push("" + (counter += 1));
}
function addItem2() {
$.set(items, $.get(items) ?? []);
$.get(items).push("" + (counter += 1));
}
function addItem3() {
$.set(items, $.proxy($.get(items) ?? []));
$.get(items).push("" + (counter += 1));
}
I see that in addItem() and addItem2() the value could be wrapped with $.proxy(...)
like it was done for addItem3():
function addItem() {
$.set(items, $.get(items) ?? $.proxy( [] )).push("" + (counter += 1));
}
function addItem2() {
$.set(items, $.get(items) ?? $.proxy( [] ));
...
In the case of a class the code is:
class Data {
#items = $.state(null);
get items() {
return $.get(this.#items);
}
set items(value) {
$.set(this.#items, $.proxy(value));
}
counter = 0;
addItem() {
(this.items ??= []).push("" + ++this.counter);
}
...
The addItem() could be:
(this.items ??= $.proxy( [] )).push("" + ++this.counter);
based on the fact that second invocation of proxy() inside the setter method will be effectively a noop. I have not come up with a good solution for this case yet.
Importance
would make my life easier