DEV Community

Cover image for The Evolution of Signals in JavaScript
Ryan Carniato for This is Learning

Posted on • Edited on

The Evolution of Signals in JavaScript

There has been some buzz recently in the frontend world around the term "Signals". In seemingly short order they seem to be everywhere showing up in everything from Preact to Angular.

But they are not a new thing. Not even remotely if you consider you can trace roots back to research in the late 1960s. At its foundation is the same modeling that enabled the first electronic spreadsheets and hardware description languages (like Verilog and VHDL).

Even in JavaScript, we've had them since the dawn of declarative JavaScript Frameworks. They've carried various names over time and come in and out of popularity over the years. But here we are again, and it is a good time to give a bit more context on how and why.

Disclaimer: I am the author of SolidJS. This article reflects the evolution from the perspective of my influences. Elm Signals, Ember's computed properties, and Meteor all deserve shoutouts although not covered in the article.

Unsure what Signals are or how they work? Check out this Introduction to Fine-Grained Reactivity:


In the Beginning...

It is sometimes surprising to find that multiple parties arrive at similar solutions around exactly the same time. The starting of declarative JavaScript frameworks had 3 takes on it all released within 3 months of each other: Knockout.js (July 2010), Backbone.js (October 2010), Angular.js (October 2010).

Angular's Dirty Checking, Backbone's model-driven re-renders, and Knockout's fine-grained updates. Each was a little different but would ultimately serve as the basis for how we manage state and update the DOM today.

Knockout.js is of special importance to the topic of this article, as their fine-grained updates were built on what we've come to call Signals. They introduced initially 2 concepts observable (the state) and computed (side effect) but over the next couple of years would introduce the 3rd pureComputed (derived state) to the language of the frontend.

const count = ko.observable(0);

const doubleCount = ko.pureComputed(() => count() * 2);

// logs whenever doubleCount updates
ko.computed(() => console.log(doubleCount()))
Enter fullscreen mode Exit fullscreen mode

The Wild West

Patterns were a mix of patterns learned from developing MVC on the server and the past few years of jQuery. One particular common one was called Data Binding which was shared both by Angular.js and Knockout.js although in slightly different ways.

Data Binding is the idea that a piece of state should be attached to a specific part of the view tree. One of the powerful things that could be done was making this bi-directional. So one could have state update the DOM and in turn, DOM events automatically update state all in an easy declarative way.

However, abusing this power ended up being a foot gun. But not knowing better we built our applications this way. In Angular without knowledge of what changes it would dirty check the whole tree and the upward propagation could cause it to happen multiple times. In Knockout it made it difficult to follow the path of change as you'd be going up and down the tree and cycles were common.

By the time React showed up with a solution, and for me personally, it was the talk by Jing Chen that cemented it, we were more than ready to jump ship.


Glitch Free

What was to follow was the mass adoption of React. Some people still preferred reactive models and since React was not very opinionated about state management, it was very possible to mix both.

Mobservable (2015, later shortened to MobX) was that solution. But more than working with React it brought something new to the table. It emphasized consistency and glitch-free propagation. That is that for any given change each part of the system would only run once and in proper order synchronously.

It did this by trading the typical push-based reactivity found in its predecessors with a push-pull hybrid system. Notification of changes are pushed out but the execution of the derived state was deferred to where it was read.

For a better understanding of Mobservable's original approach check out: Becoming Fully Reactive: An in Depth Explanation of Mobservable by Michel Westrate.

While this detail was largely overshadowed by the fact that React would just re-render the components that read changes anyway, this was a monumental step forward in making these systems debuggable and consistent. Over the next several years as algorithms became more refined we'd see a trend towards more pull based semantics.


Conquering Leaky Observers

Fine-grained reactivity is a variation of the Gang of Four's Observer Pattern. While a powerful pattern for synchronization it also has a classic problem. A Signal keeps a strong reference to its subscribers, so a long-lived Signal will retain all subscriptions unless manually disposed.

This bookkeeping gets prohibitively complicated with significant use, especially where nesting is involved. Nesting is common when dealing with branching logic and trees as you'd find when building UI views.

A lesser-known library, S.js (2013), would present the answer. S developed independently of most other solutions and was modeled more directly after digital circuits where all state change worked on clock cycles. It called its state primitive Signals. While not the first to use that name, it is where the term we use today comes from.

More importantly, it introduced the concept of reactive ownership. An owner would collect all child reactive scopes and manage their disposal on the owner's own disposal or were it ever to re-execute. The reactive graph would start wrapped in a root owner, and then each node would serve as an owner for its descendants. This owner pattern is not only useful for disposal but as a mechanism to build Provider/Consumer context into the reactive graph.


Scheduling

Vue (2014) also has also made huge contributions to where we are today. Besides being in lockstep with MobX with advances in optimizing for consistency, Vue has had fine-grained reactivity as its core since the beginning.

While Vue shares the use of a Virtual DOM with React, reactivity being first-class meant it developed along with the framework first as an internal mechanism to power its Options API to, in the last few years, being front and center in the Composition API (2020).

Vue took the push/pull mechanism one step forward by scheduling when the work would be done. By default with Vue all changes are collected but not processed until the effects queue is run on the next microtask.

However, this scheduling could also be used to do things like keep-alive(preserving offscreen graphs without computational cost), and Suspense. Even things like concurrent rendering are possible with this approach, really showing how one could get the best of both worlds of pull and push-based approaches.


Compilation

In 2019, Svelte 3 showed everyone just how much we could do with a compiler. In fact, they compile away the reactivity completely. This is not without tradeoffs, but more interesting is Svelte has shown us how a compiler can smooth out ergonomic shortcomings. And this will continue to be a trend here.

The language of reactivity: state, derived state, and effect; not only gives us everything we need to describe synchronized systems like user interfaces but is analyzable. We can know exactly what changes and where. The potential for traceability is profound:

If we know that at compile time we can ship less JavaScript. We can be more liberal with our code loading. This is the foundation of resumability in Qwik and Marko.


Signals into the Future

Given how old this technology is, it is probably surprising to say there is much more to explore. But that is because it is a way of modeling solutions rather than a specific one. What it offers is a language to describe state synchronization independent of any side effect you'd have it perform.

It would seem unsurprising perhaps then that it would be adopted by Vue, Solid, Preact, Qwik, and Angular. We've seen it make its way into Rust with Leptos and Sycamore showing WASM on the DOM doesn't have to be slow. It is even being considered by React to be used under the hood:

And maybe that's fitting as the Virtual DOM for React was always just an implementation detail.

Signals and the language of reactivity seem to be where things are converging. But that wasn't so obvious from its first outings into JavaScript. And maybe that is because JavaScript isn't the best language for it. I'd go as far as saying a lot of the pain we feel in frontend framework design these days are language concerns.

Wherever this all ends up it has been quite a ride so far. And with so many people giving Signals their attention, I can't wait to see where we end up next.

Latest comments (51)

Collapse
 
matatbread profile image
Matt • Edited

Interesting, but Signals seem to require yet more learning curve to replicate a function that all modern JS engined al tray support: async iterators.

In my library, AI-UI, iterable properties can be directly set on any JS object (including DOM nodes) that are both value and async iterable (via the Iterators.definrIterableProperty. This includes all primitives and objects. There's more info at here.

Setting such a property is just a normal assignment obj.prop = expression. Values are similarly retrieved by a simple reference, and can be subscribed, mapped and for...awaited using existing, well known JS primitives or the proposed async iterator methods obj.prop.map/filter/consume etc. (AI-UI provides implementions of these as the standard has yet to be implemented outside of core-js)

Why do we need all this new syntax to implement something already part of the web platform?

Collapse
 
ryansolid profile image
Ryan Carniato This is Learning

I mean this is an old story. Signals have existed for ages. Long before we got Async Iterators in the platform. That being said while I've never seen framework based on async iterators I have seen one based on generators: crank.js.org/.

Generally if there was any interest we'd have a framework like that. I've looked at hundreds of JS frameworks in depth and while it is possible no one has put the right combination together, nailing all the aspects seems challenging. What we do know is that Signals based approaches have a pleasant DX and some of the best performance.

AI-UI looks like it could be promising. Strongly recommend making a few of the common demos with it like TodoMVC, Hackernews, and entering the JS Framework Benchmark(github.com/krausest/js-framework-b...). Those are staple examples to give a quick idea of what one is dealing with.

Collapse
 
matatbread profile image
Matt

Thanks for the feedback Ryan.

Whether it's Signals, Observables, Events or any of the variations on this theme over the years, none of them have demonstrated enough granularity to be serious candidates as language features. Typically this is because they were initially tied to an implementation - a UI framework or similar - that understandably skewed the implemention and syntax in a specific direction.

I believe async iterators don't suffer from this legacy. A simple reading is that they are Promises that can resolve multiple times. As such, any framework like AI-UI that can accept them as template expressions, has a simplicity that is hard to beat as the language directly supports their management and manipulation.

I plan on writing an article on the next few days that demonstrates just how focused and modular they are. The AI-UI function defineIterableProperty allows the developer to define members on any object that can be set and retrieved and consumed as async iterators just using the basic JS =, . and for await respectively with no compilation step or other boilerplate or setup.

const x = { foo: 123 };
Iterators.defineIterablePropery(x, 'bar', 456);
console.log(x.bar); // 456
x.bar.consume(() => console.log('bar is ", x.bar);

// Look Ma - no weird syntax!!
x.bar -= 111; // "bar is 346"
Enter fullscreen mode Exit fullscreen mode

Hopefully you'll get a chance to read and comment!

Once again, I really appreciate you taking the time to add your views

Thread Thread
 
matatbread profile image
Matt

I should have mentioned in my bio that I used to be a hardware guy, and wrote a decent amount of VHDL in the early 2000s.

I'm aware the concept of Signals has a long history. My comment about "new syntax" was really asking if it was necessary to create a new module to implement this idea given the language gained the key features required some years ago.

Thread Thread
 
matatbread profile image
Matt

More about iterable properties in my new article here

Collapse
 
benracicot profile image
Ben Racicot

I cannot find anything on the current state of native signals in JavaScript. Is there a proposal? What is the best way to work with them now?

Collapse
 
ryansolid profile image
Ryan Carniato This is Learning

This is very very new idea. A proposal is being worked on, I'd expect some sort of initial draft published by November at the latest. At which point everyone can get involved.

Collapse
 
sjeiti profile image
Ron Valstar

I'm not that surprised about the beginning. The year 2010 was when 'Apple killed Flash' and a lot of developers were switching from ActionScript to JavaScript (both being ECMAScript). In ActionScript I was using as3-signals by Robert Penner (inspired by QT). When I went back to JavaScript I traded it for js-signals by Miller Medeiros. (there's also Robotlegs)
I've always loved signals, especially for smaller, framework-less projects. So I am surprised about it making a comeback in the bigger frameworks. Although signals now is a bit different; more state, less pub/sub.

Collapse
 
lkithakur profile image
Lakki

At my currrent level I find it difficult to understand many of the concepts/terms discussed in this article, what should I do to reach the level where I can start understanding them or atleast get a hang of what we are talking about ?

Collapse
 
lifaon74 profile image
valentin • Edited

If some of you are interested in Observables/Observers I wrote a library dedicated to them: @lirx/core, and started to work on a framework @lirx/dom. Feel free to give your feedbacks

Collapse
 
koplenov profile image
koplenov

Look at this, dude: dev.to/ninjin/designing-the-ideal-...

What do you think of this?

Collapse
 
simonmaverick profile image
Simon Thuillier

Hi, good post providing an interesting approach about the troubled history of JS frameworks for the last 15 years.

I can't help but try to link it to my current activity involving reactive programming in Java/Spring ecosystem. There is a paper from 1985 "on the development of reactive systems" (Harel & Pnueli) which already highlighted some interesting properties about this paradigm stating notably : "A reactive system does not compute or perform a function, it maintains a certain ongoing relationship with its environment. "

While i'm unsure how to link it to your post I can't help but feel a relationship. I also notice this tendency of JS frameworks to go forward the principles of the reactive manifesto (2014) : message driven, elastic, responsive and resilient architectures.

Collapse
 
seanmclem profile image
Seanmclem

Do you ever wish that Google didn’t give up on making Dart the new language of the browser?

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍

Collapse
 
alexhales67 profile image
Alexhales67

Do I have to use the Quik framework to use?
Does Quik supports react components?

Some comments have been hidden by the post's author - find out more