Train signals with mountains in the distance

Poor man's signals

Joeri Sebrechts

Signals are all the rage right now. Everyone's doing them. Angular, and Solid, and Preact, and there are third party packages for just about every framework that doesn't already have them. There's even a proposal to add them to the language, and if that passes it's just a matter of time before all frameworks have them built in.

Living under a rock

In case you've been living under a rock, here's the example from Preact's documentation that neatly summarizes what signals do:

Simply put, signals wrap values and computations in a way that allows us to easily respond to every change to those values and results in a targeted way, without having to rerender the entire application in the way that we would do in React. In short, signals are an efficient and targeted way to respond to changes without having to do state comparison and DOM-diffing.

OK, so, if signals are so great, why am I trying to sell you on them on a vanilla web development blog? Don't worry! Vanilla web developers can have signals too.

Just a wrapper

Signals are at heart nothing more than a wrapper for a value that sends events when the value changes. That's nothing that a little trickery with the not well known but very handy EventTarget base class can't fix for us.

This gets us a very barebones signals experience:

But that's kind of ugly. The new keyword went out of fashion a decade ago, and that addEventListener sure is unwieldy. So let's add a little syntactic sugar.

Now our barebones example is a lot nicer to use:

The effect(fn) method will call the specified function, and also subscribe it to changes in the signal's value.

It also returns a dispose function that can be used to unregister the effect. However, a nice side effect of using EventTarget and browser built-in events as the reactivity primitive is that it makes the browser smart enough to garbage collect the signal and its effect when the signal goes out of scope. This means less chance for memory leaks even if we never call the dispose function.

Finally, the toString and valueOf magic methods allow for dropping .value in most places that the signal's value gets used. (But not in this example, because the console is far too clever for that.)

Does not compute

This signals implementation is already capable, but at some point it might be handy to have an effect based on more than one signal. That means supporting computed values. Where the base signals are a wrapper around a value, computed signals are a wrapper around a function.

The computed signal calculates its value from a function. It also depends on other signals, and when they change it will recompute its value. It's a bit obnoxious to have to pass the signals that it depends on as an additional parameter, but hey, I didn't title this article Rich man's signals.

This enables porting Preact's signals example to vanilla JS.

Can you use it in a sentence?

You may be thinking, all these console.log examples are fine and dandy, but how do you use this stuff in actual web development? This simple adder demonstrates how signals can be combined with web components:

And here's a live demo:

In case you were wondering, the if is there to prevent adding the effect twice if connectedCallback is called when the component is already rendered.

The full poor man's signals code in all its 36 line glory can be found in the tiny-signals repo on Github.