tc39 / proposal-signals

A proposal to add signals to JavaScript.
MIT License
3.32k stars 57 forks source link

API: get/set methods and/or .value accessor #134

Open caridy opened 5 months ago

caridy commented 5 months ago

The current API relies on two methods to read or set the state, this is convenient, and in many cases, shorter (less characters) than having to do o.value = newValue. However, there are frameworks, and template engines, that do not support function calling syntax, making it extremely difficult to work with signals as they will have to either:

  1. Adopt a new syntax to support function calling (ember has the lispy syntax now {(someFn)}, but LWC doesn't have that), or
  2. Adopt a new syntax to identify signal-like objects to read the value.

To be more specific, LWC Framework uses the following syntax: {obj.x} in the HTML templates. For this to work with the current Signal API, x being a signal object, we will have to introduce a new syntax for the template engine to know that for x, it must call get() method to read out the state, or introduce something equivalent to ember's function calling syntax.

On the other hand, a property accessor is equivalent in functionality, and it is supported in every template engine out there. I wonder what's the rational here to use a function over an accessor?

/cc @wycats

fabiospampinato commented 5 months ago

IMO this isn't an actual addressable problem, because for any given possible shape that the proposed signal API could have there is going to be some framework who doesn't already support that out of the box, so a solution to this problem in general is not possible.

NullVoxPopuli commented 5 months ago

100% what @fabiospampinato said, but also: Regarding ember, no need to worry:

For reading and setting, I recommend anyone looking at this proposal start moving to the newer component format (gjs/gts) (but all of this is doable in older ember, too with this references / property assignments):

const theSignal = new Signal.State(0);
const increment = () => theSignal.set(theSignal.get() + 1);

<template>
  {{ (theSignal.get) }}

  <button {{on 'click' update}}>Increment</button>
</template>

but, that said, we're likely to have a wrapper around native signals for a few reasons, but one is that classes are good, and we'll still want to have this work:

class Demo {
  @tracked accessor value = 0;

  update = () => this.value++;

  <template>
    {{ this.value }}

    <button {{on 'click' this.update}}>Increment</button>
  </template>

}  

which is basically the same as today (tho, using spec-decorators rather than legacy).

Additionally, the verbiage/nomenclature described in https://tutorial.glimdown.com/ is still favorable (tho disclaimer: not "official", and is mostly my authoring, heavily inspired by Starbeam), and we can still have:

import { cell } from 'somewhere';

const greeting = cell("Hello there!");

// Change the value after 3 seconds
setTimeout(() => {
  greeting.current = "General Kenobi!";
}, 3000);

<template>
  Greeting: {{greeting.current}}
</template>

hypothetical integration / implementation of cell

class Cell {
  #signal;

  constructor(initial) {
     this.#signal = new Signal.State(initial);
  }

  get current() {
    return this.#signal.get();
  }
  set current(value) {
    this.#signal.set(value);
  }

  update = (cb) => {
    this.#signal.set(cb(this.#signal.get()));
  }
}

export function cell(initial) {
  return new Cell(initial);
}

and then the @tracked decorator would look like this (backcompat omitted for brevity):

export function tracked(target) {
  const { get } = target;

  return {
    get() {
      return get.call(this).get();
    },

    set(value) {
      get.call(this).set(value);
    },

    init(value) {
      return new Signal.State(value);
    },
  };
}

additionally, (and again for Ember specifically), we can register a "helper manager" so that the rendering engine does know how to "collapse" the value of a Signal, if encountered:

const count = new Signal.State(0);

<template>
  {{count}} renders 0
</template>

because count is an instanceof Signal.State, we can key off that call get() for folks (something we'd already have to do if we want the same "collapsing" behavior for the cells in ember-resources

Signals are an underlying implementation, which would replace parts of @glimmer/validator in the glimmer-vm and/or replace parts of Starbeam, which we already want to plug into the core of Glimmer.

Other things we're considering with the future of reactivity in ember is scheduling/batching reactive flushes via a unified Render-Aware Scheduler (which it would be great to see something like this also enter a proposal to TC39 or elsewhere.

caridy commented 5 months ago

Let me re-iterate my original question: "what is the rationale? why get/set is better than a property accessor for the signals use-cases?"

Additionally, what do you expect to happen here: const { get, set } = new Signal.State(0) when those functions are invoked? Looking forward to see the specs.

fabiospampinato commented 5 months ago

Let me re-iterate my original question: "what is the rationale? why get/set is better than a property accessor for the signals use-cases?"

I'm not sure I can make a strong argument for this, but a getter/setter pair tends to be more implicit than regular functions, which nowadays seem to be proposed sporadically in new proposals.

Additionally, what do you expect to happen here: const { get, set } = new Signal.State(0) when those functions are invoked? Looking forward to see the specs.

In general a goal of the proposal is minimizing memory usage, so that breaking presumably is a wanted behavior here, as if it didn't break it would mean that those methods would be 2 new functions, rather than 0 new functions since the functions are created once and read from the prototype.

Lastly I guess making standalone get/set functions to pass around, which some frameworks would need to do, is more natural to do if those are regular functions and not a getter/setter pair.

littledan commented 5 months ago

Frameworks are expected to build their ergonomics around signals; the get/set methods are not expected to be especially convenient. Personally, I like the approach of encouraging the use of reactive objects, as @NullVoxPopuli has been prototyping in https://github.com/NullVoxPopuli/signal-utils . We’re focusing initially on the core semantics, which are hard enough to unify.

I’d imagine templating systems to be around whatever JS-level wrapper, rather than focusing on the get/set methods. The get/set methods are definitely not bound; it will not work to destructure them, as implied by the protospec in the readme.

lassevk commented 5 months ago

Would a .value property be compatible with a future transition to async?