Skip to content

Nullish coalescing assignment (??=) does not work with state #14268

Closed
@kkolinko

Description

@kkolinko

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions