Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
kkolinko opened this issue Nov 11, 2024 · 1 comment · May be fixed by #14273
Open

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

kkolinko opened this issue Nov 11, 2024 · 1 comment · May be fixed by #14273
Labels

Comments

@kkolinko
Copy link

kkolinko commented Nov 11, 2024

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

@dummdidumm
Copy link
Member

I debugged this a bit, and it seems that the $.proxy(value) call inside set items(value) comes too late. It seems the getter returns a value that is not proxified yet? It's very strange, and I'm wondering if this is a browser bug or spec gotcha or something else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants