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

Feedback and Concerns on the Proposal Signals #105

Open
SukkaW opened this issue Apr 1, 2024 · 18 comments
Open

Feedback and Concerns on the Proposal Signals #105

SukkaW opened this issue Apr 1, 2024 · 18 comments

Comments

@SukkaW
Copy link

SukkaW commented Apr 1, 2024

I am writing to express some concerns regarding the proposal to incorporate signals and reactive programming into the ECMAScript spec.

While I appreciate the potential benefits of signals for managing short-lived and non-persistent states within a JavaScript application—especially in the context of developing web applications and desktop/mobile applications (where the user interactions matter), I believe it's also important to consider the implications for server-side JavaScript. Server-side applications are often either stateless or persist states in an on-disk database rather than in memory. As such, the introduction of a Signal directly into the JavaScript language may not be beneficial in these contexts. Instead, it might be more appropriate to consider a runtime-specific implementation, such as a Web API. This approach would allow developers to adopt signals where they make sense, without imposing additional complexity on the language itself.

Moreover, I would like to draw attention to the existing Proxy object in JavaScript, which already provides a low-level language agnostics for library authors to implement similar functionality. See also #101.

Lastly, I would like to point out the similarities between the proposed Signal API and the deprecated Object.observe proposal. The latter also aimed to track object mutations and provide responsive callbacks, but it was ultimately withdrawn. I worry that the new signal proposal might essentially be a reiteration of Object.observe, and it could face similar challenges and criticisms.

I believe it's crucial to thoroughly consider these points to ensure the continued robustness and versatility of JavaScript. I look forward to engaging in further discussions about this proposal and contributing to the evolution of the language.

Thank you for your time and consideration!

@EisenbergEffect
Copy link
Collaborator

Thanks for raising this @SukkaW.

I'll try to address each point from my own perspective. Other individuals may have additional thoughts.

Regarding TC39 as the vehicle for standardization. We don't have to do this through TC39. We started there because we heard non-Web use cases during our interviews, and because various members had strong prior experience with the TC39 process. That said, the largest set of use cases are UI related. If TC39 turns out to not be the right path, we can certainly shift to a different standards body.

Regarding Proxy. The Proxy, in itself, doesn't provide the infrastructure for interoperable signals. Signals have a very specific acyclic graph data structure and a "push then pull" algorithm. With proxies alone, the goals of this proposal cannot be achieved, and certainly not in an interoperable way. Signals is great paired with proxies, where a signal can be the underlying storage mechanism for a Proxy-based state store.

Regarding Object.observe(). Signals are very different from the types of observable properties and pub/sub that Object.observe() proposed. In fact, one of the biggest criticisms of Object.observe() was that it did not handle situations like what Signal.Computed handles. It had no notion of dependency tracking let alone the graph structure put forth in the signals proposal. The signals proposal also does not propose to alter the behavior of native objects in any way, another criticism of Object.observe() and Array.observe(). In many ways, signals is lower level. You could implement Object.observe() and Array.observe() in userland by leveraging signals.

@SukkaW
Copy link
Author

SukkaW commented Apr 1, 2024

Thank you for your reply and for addressing the points I raised!

Regarding TC39 as the vehicle for standardization. We don't have to do this through TC39.

Regarding Proxy. The Proxy, in itself, doesn't provide the infrastructure for interoperable signals.

With proxies alone, the goals of this proposal cannot be achieved, and certainly not in an interoperable way.

The main argument here is that if it's already possible to implement the functionalities and the behaviors of Signal using only setters, getters, and Proxy (whether it is possible, we can continue the discussion at #101 for easier tracking), then it may not be necessary to introduce it to the language.

This is similar to why the low-level data structures like Linked List and the Queue are not included in the ECMA-262 spec and W3C/WHATWG standards. They are useful, but JavaScript has already provided low-level building blocks that developers can use to create such libraries. The runtime will eventually optimize the execution if the approach and the code path become popular.

@SukkaW
Copy link
Author

SukkaW commented Apr 1, 2024

IMHO, what JavaScript really requires is a low-level building block designed for incremental computation. And I understand it is the key element that Signal.Computed was trying to add to the language.

What JavaScript doesn't need is a specific set of APIs only for building application responds to user interactions.

@Jack-Works
Copy link
Member

My thoughts: The successful lesson is Promise, which was standardized in tc39 and used when the async function was proposed. A lesson learned from the failure is AbortSignal, which is standardized on the Web but has extensive needs in the entire ecosystem. This leads to the need for cross-environment coordination like WinterCG to solve the problem of cross-ecological consistency, but this actually shows that it should be implemented in tc39 Standardization (note that fetch is different because fetch involves IO, but AbortSignal does not).

I'm not sure signal has widespread use outside of rendering, so I'm not sure it should be in tc39.

@ljharb
Copy link
Member

ljharb commented Apr 1, 2024

I agree that Promise is a success and AbortSignal is not, and the venue is largely why that’s the case.

Rendering is very much also a server-side concern, so i’m much more optimistic that signals belongs in TC39.

@SukkaW
Copy link
Author

SukkaW commented Apr 1, 2024

Rendering is very much also a server-side concern, so i’m much more optimistic that signals belongs in TC39.

As I have already pointed out initially, the Signal is good for:

managing short-lived and non-persistent states within a JavaScript application—especially in the context of developing web applications and desktop/mobile applications (where the user interactions matter)

The most common usage of those short-lived and non-persistent states are implementing user interactions, and that's it.

As for the server-side javascript and the rendering, here I quote from my initial feedback:

Server-side applications are often either stateless or persist states in an on-disk database rather than in memory. As such, the introduction of a Signal directly into the JavaScript language may not be beneficial in these contexts.

We are not making persistent state (E.g. on-disk databases like MySQL, SQLite, PostgreSQL) reactive, right?

@Jack-Works
Copy link
Member

Rendering is very much also a server-side concern, so i’m much more optimistic that signals belongs in TC39.

SSR is pretty active in today's Web (nextjs etc) but let's say, should it also exist in the XS engine? Will you use it in general computation? (It's a maybe for me because I can see the benefit of incremental computing).

@SukkaW
Copy link
Author

SukkaW commented Apr 1, 2024

I can see the benefit of incremental computing

JavaScript does need a low-level building block for implementing incremental computation (as I have already mentioned in #105 (comment)), but it doesn't necessarily be a new Signal.Computed().


The computed, or useMemo, or React Forget, are up to the frameworks and libraries. Those utilities don't necessarily need to exist within the language itself, but could be implemented using the low-level building block (Just like how Proxy powers reactive framework).

@ljharb
Copy link
Member

ljharb commented Apr 1, 2024

The most common usage of those short-lived and non-persistent states are implementing user interactions

It's equally common in a webserver to respond to requests - they're often stateless, modulo a database, across requests, but quite stateful during the many distinct and separately located parts of a given request/response.

Just like how Proxy powers reactive framework

Proxy is incredibly slow and nearly impossible to optimize, and arguably it existing is a mistake. I don't think that's a good model to inspire future proposals.

@SukkaW
Copy link
Author

SukkaW commented Apr 1, 2024

Proxy is incredibly slow and nearly impossible to optimize, and arguably it existing is a mistake. I don't think that's a good model to inspire future proposals.

Indeed, Proxy is slow, and JavaScript has failed to provide a high-performance low-level building block for creating Reactivity Utilities. However, this doesn't imply that we need to incorporate Reactivity Utilities directly into the language. JavaScript itself doesn't need Reactivity Utilities; they should be implemented by the users of the language in the form of libraries or frameworks. The same goes for the data structures like Linked List and Queue, they are useful, but they are not part of the ECMA-262 spec (yet?).

The same principle applies to Computed. What JavaScript needs is a low-level building block for implementing Incremental Computation (upon which computed, createSignal, and useMemo would be built), not a new Computed().

@runspired
Copy link

Most non-rendering JS APIs I've worked on have almost immediately hit the desire for signals, primarily from the perspective of memo. Same for build tooling. Plenty of ways to construct a good memo function obviously, but none quite as robust as you get with signals.

@SukkaW
Copy link
Author

SukkaW commented Apr 2, 2024

primarily from the perspective of memo

@runspired

If you are talking about something like this:

// A magic function `lazy()`.
// a() function will only execute when it is being referenced,
// and it will only execute once
const a = lazy(() => { /* something */ });

if (/* condition a */) {
  /* ... */ = a();
} else if (/* condition b */) {
  // no a();
} else {
  /* ... */ = a();
}

if (/* condition c */) {
  /* ... */ = a() + 1;
}

// a() will never evaluate if only condition b is truthy
// a() will only evaluate once even if both condition a and c is truthy

Then what you need is the incremental computation, not the derived state.

@lifaon74
Copy link

lifaon74 commented Apr 5, 2024

I feel truly mitigated: signals are amazing, and (in my opinion), the best solution (so far) for web applications. Giving them the opportunity to exist as a native API is great for developers and performances.

However, I clearly see some limits:

  • Observables on their time were the "way to go" for reactive applications, and we even get a proposal (discontinued), but paradigms evolve and we always find new approaches to solve particular problems. Signals are amazing now, but they may be replaced by an even better approach in 2-3 years maybe.
  • this proposal introduces some sort of low level API. We're far from the simplicity of promises, here we have access to fine grained controls to permit wide and different implementations. This is something unusual. Usually, to get quick adoption, simplicity is better.
  • signals are new, and I feel like they are not mature enough yet for a proposal (which will "fix" them definitively): for example, the proposal doesn't permit signals to throw, but we may imagine such requirement in the future (for example, we may expect that getting a uninitialized signal throws; or, if a signal proxifies an HTTP response, and this last one is errored, then we may want a way to set the signal in an errored state too). As they are new, we still have not explored all their limits, and not created yet the "optimal" way to use them (maybe higher order functions and usages not created now, may exist in a near future).

Finally, if it's part of the tc39 instead of w3c, we may thing about more "integrated" signals (and let the engine do the magic):

signal a = 5; // equivalent of let a = 5
signal b = computed {
  return a + 2;
}

const unwatch = watch {
  console.log(b);
}

TC39 opens the gate to new language possibilities and integrations, where the w3c is better suitable for to API.
However, such changes would require extremely robust specs, and a lot of "intelligence" behind, to create something both universal, and future proof. If it fails, this could cost a lot to the js ecosystem.

In my opinion, I suggest, it may be worth waiting 1-2 years for a real stabilisation and wide adoption (thus exposing limits, and introducing improvements), before jumping immediately into something still in early stages.

@joakim
Copy link

joakim commented Apr 5, 2024

IMHO, what JavaScript really requires is a low-level building block designed for incremental computation.

@SukkaW Would you prefer to break this proposal into:

  • A TC39 proposal for low-level State and Computed classes (or some equivalent language feature)
  • A W3C proposal for a global Signal object utilizing the above (basically Signal.subtle)

I think that would be a logical split.

Two proposals entails more work for the authors, but it would greatly simplify the TC39 proposal, possibly increasing the chances of approval and inclusion in the standard. The Signal object might also be easier to accept by W3C when standing on the shoulders of a standard language feature.

I'm excited about this proposal, and I hope it lands somehow. State management is such a critical part of a language. This reactive approach has emerged over time as the preferred way to manage state in interactive JS applications, so I think it makes sense to standardize and provide a common ground for interoperability. State and Computed are powerful "primitives" on their own, I think they're well within the realm of language specification.

@SukkaW
Copy link
Author

SukkaW commented Apr 5, 2024

I think that would be a logical split.

That would be awesome!


Also, some other thoughts:

The incremental computation must be a language feature, not a built-in class. Because class introduces js object, js object introduces memory and GC pressure, memory and GC pressure introduces performance overhead, tick-tack-toe.

@joakim
Copy link

joakim commented Apr 5, 2024

I think engines can optimize away what appears to be an object in this case. They're not bound to represent it as an object in memory, as long as it functions according to spec. So performance shouldn't be an issue, at least in the popular, highly optimized engines. It's not the syntax, but the semantics of the proposal, how the graph is to be implemented, that will affect performance.

Classes do have some benefits.

  • More likely to be accepted by TC39 than new keywords/syntax
  • More likely to receive support by engines (easier to implement)
  • Much easier to polyfill (doesn't require a transpiler)
  • They're extensible and typable

While special syntax would be nice, I just want this proposal to get accepted :)

@phryneas
Copy link
Member

phryneas commented Apr 5, 2024

Responding from a library author perspective:
If Signals would only land in the browser, but not other engines, we would not be able to adopt them.

  • Redux could make great use of Signals in selectors. But Redux code is used in React applications. React applications run through SSR. Even though they would not get a lot of "reactive" use during SSR, we would rely on the presence of the same APIs.
  • The same can be said for Apollo Client - Signals would be very valuable in many places there. But if we only could use them in the browser, we couldn't use them at all.

Polyfills are not a happy fallback here - think of fetch where we now have the browser's fetch API, undici, node-fetch and half a dozen others, all of them with slight differences. Just swapping one for the other can make for subtle memory leaks.

And that ignores the fact that both Redux and Apollo Client are even used in normal backend code - completely outside of React applications.

Also, going the route of "have the ecosystem establish a library for it instead of including it in the platform" is probably not a good idea - because it might not happen, and in the end you'll ship multiple different implementations to your users.

Example case: If you use Apollo Client in Angular nowadays, you will ship Zen-Observable (because Apollo Client) and RxJs (because of Angular) to your users.

=> I'm very much in favor of this going through TC39.

@alinnert
Copy link

alinnert commented Apr 6, 2024

I also want to add that environments like Node.js shouldn't be seen as a synonym for CRUD servers. @phryneas has already mentioned frontend tooling. But there are even more fields like more action/task heavy servers, AI, IoT, hardware development, CLI tools, etc. which would not be implemented in a browser.

Take this as an example:

const counter = new Signal.State(0)

hardwareButton.addEventListener('push', () => {
  counter.set(counter.get() + 1)
})

effect(() => {
  printOn7SegmentDisplay(counter.get())
})

This could be the code for some click counter. The structure for applications like this is very close to what a browser application would look like. Just replace html buttons with hardware buttons and print the result to some special screen instead of the DOM.

I also want to mention that I'm very happy to see this being worked on. My main use case might be browser centric as well but I'm playing around with how I could develop a web application with as few libraries as possible. And I figured out one of the biggest pain points is indeed state management. I've even started working on a library to tackle this problem and the result looks extremely similar to this proposal.

If this proposal could be made more simple like some mentioned I'd be all here for it though.

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

No branches or pull requests

9 participants