Signals Are Everywhere
- History Of Signals
- What are signals?
- Implementing a signal-based library
- Using JSX
- Conclusion
- Other resources
Vue, Preact, Solid, Qwik, Angular and now Svelte. It seems like every front-end framework is getting signals. What are they?
In this post, we’ll learn about signals by implementing a simple signal-based reactive library.
History Of Signals
Signal-based reactivity is not a new concept. The foundations of reactive programming were laid in the 1970s with the development of electronic spreadsheets and hardware description languages. Also in our context, building reactive UIs, the use of signals – also known by various names – has been around for a while.
In 2010, Knockout.js introduced fine-grained updates with the use of observables. In 2012, S.js introduced its own primitive for reactive updates, called signals. Other not-so-popular libraries have had their own signals all this time, but it wasn’t until recently that they seem to be coming back into fashion.
Vue, Solid, Preact, Qwik, and Angular, have all introduced their own versions of signals. The announcement of Svelte 5 with their so-called “runes”, is what prompted me to write this article.
What are signals?
Signals represent a single value that changes over time. It is an atomic reactive primitive that updates its value when its dependencies change.
Signals unlock fine-grained reactivity. The following examples will be using Solid.js, but the general concepts apply to any signal-based library.
Take the following example:
This looks pretty much like a React component, and if you are familiar with it, you might be wondering what is the difference. There are a few of them, for example:
How many times would the message be logged to the console?
Once. The function only ever runs once, when the component is mounted. that makes simpler to reason about the component’s behavior. for example, we can do this:
The count
will be updated every second, and so will the button
’s text, but
only that will re-run. The console will still only log once. We have achieved fine-grained reactivity.
Here’s another one. How would we change the count state to be global state in this example?
Just move it out of the component. State and component lifecycle are not tied together.
How would we produce a side effect when the count changes?
No dependency arrays. Components only ever run once, so there is no need to specify dependencies. The effect will run every time the count changes, and only then.
This might seem like some kind of magic, maybe a compiler trick, but it is not. The concept itself is fairly simple. Let’s build our own (naive) implementation of a signal-based library.
Implementing a signal-based library
The first thing we need is… you guessed it, our signal. A signal consists of a getter, a setter and a value. We’ll follow Solid’s convention and return them in a tuple.
We need to keep track of the signal’s subscribers. We’ll use a Set
for that.
A global stack to keep track of the current observer.
And our effect function, wich will push itself onto the stack, run the callback, and then pop itself off the stack.
In the signal’s read
function, we’ll get the current observer and add it to the
subscribers list.
And in the write
function, we’ll also call every subscriber after the value is
updated.
That’s our very basic implementation of a signal and an effect.
I will recap the code now:
Let’s explain, step by step, what happens while we run the following code:
- We create a signal with the value
0
. - We create an effect with a callback that logs the count.
- The effect pushes itself onto the stack and runs its callback.
- While running the callback, the
count
function is called. - Because the
count
read function is called while there is an effect running, the effect is now the latest observer, and it is added to the subscribers list of the signal. - When we call the
setCount
function, we call the write function of the signal, which updates the value, then calls every subscriber. - We have the effect callback in the signal’s subscribers list, so it is called.
- The effect callback logs the count, which is
1
.
We have a reactive system working! We could manually create HTML
elements and
update their values using effects:
We have achieved a basic signal-based reactive system. But now we could make it easier to use, so that we don’t have to manually create elements and update their values imperatively, but rather declaratively, as most frameworks do. Let’s use JSX. Our end result should look like this:
Doesn’t that look familiar? I am sure it does.
If you only wanted to know about what signals are, and how they work, you can stop reading here. The rest of the article will explain how to implement JSX transpiling, so you can see how it works under the hood, and how we actually have achieved a (very) basic version of a VDOM-less, signal-based UI library. If that is not the case, let’s continue.
Using JSX
JSX is a syntax extension to JavaScript that allows us to write HTML-like code in JavaScript. it is used by React, Preact, Solid, Qwik, and many other libraries.
Fortunately, we have transpilers that can transform JSX into JavaScript, so we don’t need to implement it ourselves. We’ll use Babel to transform our code.
What happens under the hood when we use JSX in React?
Every HTML-like tag is transformed into a bunch of function calls. For example, the following JSX:
Gets transformed into:
Where the first argument is the tag, the second is the props, and the third
is the children. We’ll just use the @babel/plugin-transform-react-jsx
plugin to
transform our code, and we’ll implement our own createElement
function.
I will npm init
a new project, and install the following dependencies:
I’ve named this example library “Junk”, and I’m sure you know why. Anyway, let’s
change the babel configuration, to use the plugin-transform-react-jsx
with our
custom createElement
function:
Now we need to implement the createElement
function. It will receive the
tag, the props, and the children, and it will return an element object.
The addAttributes
function is just a helper to add the attributes to the
element, like event listeners, class names, and other attributes.
In the createElement
function, if the child is a function, we create an
effect that will run only when the expression changes (if it is a
signal), and will update the element with the new value.
The createExpression
function changes the element’s value. It is super
flawed, and does not take into account important things, but it works for
our basic toy example.
Now that we have this createElement
function, we can use JSX, and it will be
transformed in a bunch of calls to our function.
We can implement a simple render
helper to append the root element of our application
to the entry point on the DOM:
And similarly to React (or any other SPA), the entry point of our application
will be a div with the id root
. our JavaScript code will simply be loaded in our
index.html
file. It will load, create the elements, and append the root element to the DOM.
Our basic UI library now looks like this:
Conclusion
That’s it! We have a working signal-based library, and although it is clearly flawed and incomplete, it is a good example to learn about signals, reactivity, JSX, and how some of these things work (at a basic level) under the hood, in a practical way.
Signals are a really interesting concept that I am really excited about. I hope you enjoyed this article, learned something new, and most importantly, had fun!
You can find the code for this example here.
Other resources
This article is heavily inspired by the great work of Ryan Carniato and the Solid team. I also recommend you to check out the following resources: