⚠️ If I've missed anything, or am wrong about something, please let me know in the comments ❤️

Quick Links:

tl;dr:

  • Signals enables many ecosystems to start interoping together, even if they are completely isolated today.

What you can do?

  • Get involved, try out the polyfill, try to integrate it in to you framework (in abranch) ✨

Signals have been proposed to TC39 to be built in to JavaScript as a language native feature.

But what does this actually mean for you and your daily work?

  1. Who is this for?
  2. Why am I talking about this?
  3. What even are Signals?
  4. Why should I care?
  5. What are the requirements?
  6. Seems like there are missing features 🤔
  7. How this affects you
    1. library authoring
    2. framework maintenance
    3. application dev
  8. How you can get involved

Who is this for?

There are generally three types of roles a person can play when it comes to programming -- and these may seem obvious, but it's important that we be on the same page, because it's easy (especially for me!) to forget which audience we're talking about in which situations -- feel free to skip to the next section:

Framework authors

These are the people who will jump through any hoop in the name of ergonomics, performance, compatibility, you name it. These people work directly on projects like Angular, Ember, Preact, Solid, Svelte, Vue, and many more.

Library authors

These are people who may want their library to be usable in multiple / all frameworks that are supporting Signals -- traditionally in order to have broad reach across every framework, libraries would need to either have no reactivity, or private reactivity -- which isn't integrated into the host application, and in either case, if these libraries render anything, the host framework must be hands off, as the library "takes over" the dom element that it is given and multiple renderers cannot control the same subtree of the DOM -- this is common in Charting libraries, or older jQuery based libraries. Some libraries could provide an abstraction over reactivity and then adapters for each reactivity system -- Vue, Svelte, Angular, etc.

Application developers

These people could use the TC39 Signals directly, but more likely that their framework or ecosystem used for their application will provide wrapper utilities that align with the framework/ecosystem's existing nomenclature and mental models. For example, Svelte has runes, and Vue has ref, and Ember has tracked -- all of these can wrap the TC39 Signals, and in a minor, non-breaking, version upgrade of your framework's core library, could suddenly have support for Signals and the broader reactive ecosystem -- and you wouldn't even know without reading the changelog! 🎉

Why am I talking about this?

I, @NullVoxPopuli, am very excited about Reactivity[2].

There has been a problem in JavaScript since the dawn of time (1995): as ecosystems develop (and specifically what I am presently concerned about: web apps (which, kind of, moves our dawn of time to the mid 2000s)), they cannot share code between each other. React can't share code with Vue, nor can Svelte share code with Vue, or Ember, Angular, Solid, etc.

Billions of lines of code have been duplicated across these ecosystems solely[3] because the reactive systems are separate.

There are 3 major categories for which I can place my excitements:

  1. Interoperability, solving the above-described problem
  2. Technically, the way the reactive algorithm works (even as the TC39 proposal changes over time) is super exciting, and something I've been using myself for years in Ember.
  3. Adoption - Non-breaking, non-major change[5] to introduce Signal compatibility to a framework.

It's intended for framework and library authors to swap out their internals with the proposal's primitives (shedding tons of kb, and beginning the Epic Quest of aligning on all the similarities between our frameworks - eliminating (probably) Petabytes of network transfer of (basically) duplicate code each year).

What even are Signals?

Signals are a means to enable efficient computation of state for a particular viewer. The viewer in many cases is the DOM, but could extend to any observable[1] boundary, a WebSocket, Database Connection, etc.

The exact way in which this works isn't important right now (and also the exact details will probably change over time as the proposal is still in Stage 1) -- perhaps a topic for another time!

Some helpful terminology and definitions that various folks use

As people talk about Signals, auto-tracking, Runes, or other forms of reactivity, it may be heplful to have everything defined / so we can all be on the same page / reduce any potential ambiguity or uncertainty. Not all of these will be used here, in this post, but… more for your (and my own) information[6].

term meaning
node part of the reactive graph
value an alias of node
cell could be an alias of node, but is often root state, part of the spreadsheet analogy
root state a value representing state, it cannot depend on anything within the graph, but any number of other nodes in the reactive graph may depend on it. This is different from a render root, which would be the top element-node of your app.
atom an alias of root state
derived data a derivation on or of state(s), which may or may not be cached, in a pure form this could be written as derived = f(state)
computed alias of derived, tho this has had different meanings throughout the years in different ecosystems. In this context, specific to the proposal, it is a cached derived value.
formula alias of derived, part of the spreadsheet analogy
upstream the node that is "upstream of" a given node is closer to the root.
downstream the node that is "downstream of" a given node is further away from the root.
sink a consumer of sources or values
source root state, the source of a value
auto-tracking the process of determining which nodes a computed or derived node depends on by running it and recording which other nodse were read
lazy evaluation work is only performed when needed
steady state re-reading derived values or root state does not cause a change in output
invalidated a node in the reactive graph is marked as dirty
dirty a node in the reactive graph will be re-evaluated next time it is read
settled alias of settled state
consumed a derived node has, during evaluation, encountered nodes to depend on
effect a "side-effect" running code when something changes -- in practice this has many definitions and semantics
entangled a node in the reactive graph is depends on another node in that same graph, part of the quantum physics analogy
push an update strategy initiated by upstream nodes, which tell downsteam subscribers that they changed.
pull an update strategy initiated by downstream nodes, which ask upstream dependencies whether or not they changed -- i.e.: when updating a value, the fact of that change is not realized unless the "renderer" accesses that value.
Cached the computation or function will not run again unless the consumed values within change
Runes What Svelte calls their Signals
Tracked properties What Ember calls their Signals
refs What Vue calls their reactive values

To continue the quantum physics analogy:

A signal graph is in a superposition of states, but you can't tell because the second you try to observe them the wave collapses

but you can create double slit experiments.

wycats.

Why should I care?

From gbj in the Discord

the proposal is designed to be the greatest common factor of the existing mainstream framework signal approaches, not the least common denominator

It’s “how much could we share?” not “how little can we get away with?”

Imagine, a reactive library could be built in a such a way, using native Signals, that it works across UI Frameworks: Vue, Svete, Ember, (etc), Node CLI applications, databases, websocket implementations, etc. This proposal has the opportunity to dramatically reduce the amount of duplicate code we have between our different JavaScript ecosystems.

Some libraries that I think could greatly benefit from a shared signal implementation (provided target frameworks also natively integrate with native signals):

  • TanStack family of projects.
  • Charting libraries, such as D3.js.
  • Any project using web components.

I don't have data, as this would be very hard to measure, but I like guessing, so I'm guessing: if we had standard / platform / native signals 6 years ago, we could have saved at least a trillion dollars in duplicate library development consts, research and development, etc. Now, none of this proposal would be possible without those 6+ years of research, exploration, collaboration, and trying to out-do one another 😉.

One thing that will be interesting longer term is how web components could maybe one day be ergonomic to author without the use of a wrapper library (Lit, for example (which helps fix some of web components' ergonomics issues)).

There are two places to watch for developments there:

What are the requirements?

  • Autotracking - the main feature of the Signals proposal that makes it more than just values and functions.
  • Memory allocations should be kept to a minimum.
  • Signal.State should be extensible with fewer object allocations so that frameworks can add their own functionality on top.
  • multiple types of effects and batching should be implementable by framework authors.
  • Writes are synchronous, and immediately take effect. At no point in the reactive graph would reading a descendent, deriving from some ancestor, be out of sync.

What are the considerations?

This is just a snippet of a much bigger list of goals, considerations, and desires for the Signals proposal:

  • The proposal is minimal, and intended to be added on to later -- Changes to a language take a lot of time, so like a pull request, the smaller it is, the higher the chance it will progress towards being accepted.
  • Computation must be glitch-free, so no unnecessary calculations are ever performed.
  • JS Frameworks have their own scheduling, and the native signals implementation should enable that. There is likely no way to write a "scheduler" that efficiently integrates with a framework's own dynamic tree better than the framework native scheduling.
  • Enable composition of different codebases which use Signals -- aka / cross-framework library sharing.

There are many more of these, and if you want to learn more about this, check out the Design goals for Signals.

Seems like there are missing features 🤔

The proposal, at the time of writing, is still in Stage 1, so just about everything is subject to change and nothing is set in stone.

If you want to help out with the proposal, please open an issue or PR for collaboration 🎉

How this affects you

library authoring

Here is a demo of Signals in React and Svelte by Jack Herrington -- showing that we can make cross-framework libraries.

  • reactive code using what's in the proposal
  • no need for abstractions
  • must subscribe to "derived" purism (no effects).

In universal code, we can't use effects, because an effect usually has framework-specific semantics that don't make sense in other frameworks -- for example, if you use React's effect, the whole library may as well be react-specific. As a library author, you could circumvent this restriction by requiring your users to call a destructor at time that makes sense for your library, which calls the Watcher's unwatch method.

example usage:

import { Signal } from 'signal-polyfill';
import { signalFunction } from 'signal-utils/async-function';

const url = new Signal.State('...');
const signalResponse = signalFunction(async () => {
  const response = await fetch(url.get()); // entangles with `url`
  // after an away, you've detatched from the signal-auto-tracking
  return response.json(); 
});

// output: true
// after the async function completes
// output: false
<template>
  <output>{{signalResponse.isLoading}}</output>
</template>

Aonther util that enables more powerful fetch / AbortController management, is the Relay. (See Signal Relays by @pzuraq).

export const fetchJson = (url) => {
  return new Signal.Relay(
    { isLoading: false, value: undefined },
    (set, get) => {
      let controller;

      const loadData = async () => {
        controller?.abort();

        set({ ...get(), isLoading: true });

        controller = new AbortController();
        const response = await fetch(url.get(), { signal: controller.signal });
        const value = await response.json();

        set({ ...get(), value, isLoading: false });
      }

      loadData();

      return {
        update: () => loadData(),
        destroy:() => controller?.abort(),
      }
    }
  );
}

The Relay pattern provides a way to have more control over the behavior of things-with-lifetime, such as a fetch request. Definitely checkout pzuraq's blog post for more details.

For now, there is an exploratory signal-utils library with a bunch of reactive utilities and data structures -- pull requests are very encouraged here!! It'd be equally awesome to see other experiments people come up in their own libraries! (Noting that we don't want to accidentally make a new framework, but moreso want to test out the capabilities of the polyfill / current design of the proposal and validate ergonomics / problems / etc).

framework maintenance

The dream is that once frameworks integrate with native signals, whole ecosystems begin to start being able to share resources with one another.

Especially as it is still the early days, the most useful thing now with respect to the proposal is to try out the polyfill in your framework, replacing your own reactive primitives with those from the polyfill and open issues such as:

Here is an example of @lifeart's spike to add the signal polyfill to their renderer[7]: glimmer-next#114, and in doing so, a problem was found where unwatch performance worsened by ~ 3000% (oops).

Effects and fine-grained rendering can be implemented via a watched Computed like here in glimmer-next -- this whole PR is probably a good reference for the amount of work involved in swapping the core reactive primitives.

Higher level abstractions should also be possible, and some of these are explored in signal-utils. For example, in ecosystems that encourage classes, they may implement a decorator which allows "just regular property access" to continue working:

import { signal } from 'signal-utils';

class State {
    @signal accessor #value = 3;

    get doubled() {
        return this.#value * 2;
    }

    increment = () => this.#value++;
}

let state = new State();


// output: 6
// button clicked
// output: 8
<template>
  <output>{{state.doubled}}</output>
  <button onclick={{state.increment}}>+</button>
</template>

Another example, for reference, could be the reaction() utility from MobX.

application dev

For now there isn't much to do directly with the proposal, but you can work with your framework ecosystems to see how you can help test things out and provide feedback! This isn't to say that you wouldn't be able to use native Signals in your application once everything ships, and is integrated, etc, -- on the contrary! -- but moreso that you'll likely want to stick with your framework's specific nomenclature, utilities, etc.

How you can get involved

On this call for participation on the proposal github, you'll find links to

In summary, many / most contributions are welcome:

  • fixing typos in the README
  • adding tests
  • playing with the polyfill (finding bugs, fixing them, etc)
  • Document use cases for or against certain reactive programming patterns (and if the current design supports those use cases)
  • Add to the interactive tutorial here
  • viewable here

and more! (see the linked issue)

You can also try out signals for yourself here at this JSBin.


There is a lot to do!, but we'll get there! Between finding alignment with the broadest group possible, time to implement and adopt, and then adding additional proposals that build on top of the Signals proposal, this is my favorite meme:

Screenshot from Dune: Part 2, a Bene Gesserit, reminding us that their plans are measured in centuries

(from Dune: Part 2)

References

…and additional reading:

In my experience, the more you use them, esp as your app scales, the more their DX shines

Signals are not just about perf /1 from X/Twitter

[1]: this definition of observable is simply "noticeable" / "to be noticed" and in no way is intended to correlate to the the programming concept by the same name. Observables as a pattern (rx.js) invert the reactive graph from Signals, and have proven to be less efficient. They are a useful tool!, just not for everything (Angular has been moving to Signals from rx.js, for example).

[2]: But I haven't been the best at communicating -- written, spoken, or otherwise -- about my excitement for reactivity -- a lot of concepts are quite abstract, and a hard to find the right balance of distilling a problem to something digestible verses finding something complex enough where people don't immediately fall in to "but this other way is much easier, or could be done like 'this'".

[3]: maybe not solely, exactly. I'm sure there is a good amount of "I could do better" and "There are bugs, how hard could it be[4] to do myself"?

[4]: Anyone else have this genetic trait where they generously undeestimate effort of a task? (not just in programming) .

[5]: See also: Editions, an extension to SemVer.

[6]: This table alone, with examples in the wild could / should probably be its own post -- it's a lot of information.

[7]: this is an alternate DOM renderer for Ember focused on performance, and trying to incrementally gain as much compatibility as possible where it makes sense. I've opened an issue on Ember for a Swappable Renderer to help explore what this could be capable of, what compatibility is missing, etc -- it's still early, and the ideas still need to be proved out in big projects.

[8]: Starbeam is not 1.0 yet, and does not presently have a production ready release, so folks should hold off on trying it out. The docs even mismatch a little at this point, so learning it would be a smidge confusing without diving in to the code as the source-of-truth for learning.