marioph ✨

← Back

Signals Are Everywhere


  1. History Of Signals
  2. What are signals?
  3. Implementing a signal-based library
  4. Using JSX
  5. Conclusion
  6. 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:

counter.jsx
function Counter() {
  const [count, setCount] = createSignal(0);
  const increment = () => count(count() + 1);
 
  console.log("I only log once");
 
  return <button onClick={increment}>{count()}</button>;
}

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:

counter.jsx
function Counter() {
  const [count, setCount] = createSignal(0);
  const increment = () => setCount(count() + 1);
 
  setInterval(increment, 1000);
  console.log("I only log once");
 
  return <button onClick={increment}>{count()}</button>;
}

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?

counter.jsx
const [count, setCount] = createSignal(0);
 
function Counter() {...}
function OtherComponentThatSharesState() {...}

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?

counter.jsx
function Counter() {
  const [count, setCount] = createSignal(0);
  const increment = () => setCount(count() + 1);
 
  createEffect(() => {
    console.log('Count changed', count());
  });
 
  return <button onClick={increment}>{count()}</button>;
}

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.

signals.js
function createSignal(value) {
  const read = () => {
    return value;
  };
  const write = (newValue) => {
    value = newValue;
  };
 
  return [read, write];
}

We need to keep track of the signal’s subscribers. We’ll use a Set for that.

signals.js
function createSignal(value) {
  const subscribers = new Set();
  // ...
}

A global stack to keep track of the current observer.

signals.js
const context = [];
 
function getCurrentObserver() {
  return context[context.length - 1];
}

And our effect function, wich will push itself onto the stack, run the callback, and then pop itself off the stack.

signals.js
function createEffect(callback) {
  const effect = () => {
    context.push(effect);
    try {
      callback();
    } finally {
      context.pop();
    }
  };
 
  effect();
}

In the signal’s read function, we’ll get the current observer and add it to the subscribers list.

signals.js
function createSignal(value) {
  const subscribers = new Set();
 
  const read = () => {
    const observer = getCurrentObserver();
    if (observer) {
      subscribers.add(observer);
    }
    return value;
  };
 
  //...
}

And in the write function, we’ll also call every subscriber after the value is updated.

signals.js
function createSignal(value) {
  const subscribers = new Set();
 
  const read = () => {
    const observer = getCurrentObserver();
    if (observer) {
      subscribers.add(observer);
    }
    return value;
  };
 
  const write = (newValue) => {
    value = newValue;
    subscribers.forEach((sub) => sub());
  };
 
  return [read, write];
}

That’s our very basic implementation of a signal and an effect.

I will recap the code now:

signals.js
const context = [];
 
function getCurrentObserver() {
  return context[context.length - 1];
}
 
function createSignal(value) {
  const subscribers = new Set();
 
  const read = () => {
    // 5.
    const observer = getCurrentObserver();
    if (observer) {
      subscribers.add(observer);
    }
    return value;
  };
 
  const write = (newValue) => {
    // 7.
    value = newValue;
    subscribers.forEach((sub) => sub());
  };
 
  return [read, write];
}
 
function createEffect(callback) {
  const effect = () => {
    // 3.
    context.push(effect);
    try {
      // 4.
      callback();
    } finally {
      context.pop();
    }
  };
 
  effect();
}

Let’s explain, step by step, what happens while we run the following code:

index.js
// 1.
const [count, setCount] = createSignal(0);
 
// 2, 8.
createEffect(() => console.log('count changed', count()));
 
// 6.
setCount(count() + 1);
  1. We create a signal with the value 0.
  2. We create an effect with a callback that logs the count.
  3. The effect pushes itself onto the stack and runs its callback.
  4. While running the callback, the count function is called.
  5. 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.
  6. When we call the setCount function, we call the write function of the signal, which updates the value, then calls every subscriber.
  7. We have the effect callback in the signal’s subscribers list, so it is called.
  8. 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:

index.js
const [count, setCount] = createSignal(0);
 
const h1 = document.createElement("h1");
 
createEffect(() => h1.textContent = count());
 
const button = document.createElement("button");
button.textContent = "Click me!";
button.addEventListener("click", () => setCount(count() + 1));
 
document.body.append(h1, button);

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:

counter.jsx
function Counter(props) {
  const [count, setCount] = createSignal(props.initialCount);
  const increment = () => setCount(count() + 1);
  const decrement = () => setCount(count() - 1);
 
  createEffect(() => console.log('Count changed', count()));
 
  return (
    <section>
      <h1>The count is: <span>{count()}</span></h1> 
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </section>
  );
}

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:

hello-world.jsx
<div>
  <h1>Hello, world!</h1>
</div>

Gets transformed into:

bundle.js
React.createElement('div', null, React.createElement('h1', null, 'Hello, world!'));

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:

npm install @babel/core @babel/cli @babel/plugin-transform-react-jsx

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:

package.json
{
  "name": "junk",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "build": "babel index.jsx -d dist"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.23.0",
    "@babel/core": "^7.23.0",
    "@babel/plugin-transform-react-jsx": "^7.22.15"
  },
  "babel": {
    "plugins": [
      [
        "@babel/plugin-transform-react-jsx",
        {
          "pragma": "Junk.createElement",
          "pragmaFrag": "Junk.Fragment"
        }
      ]
    ]
  }
}

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.

render.js
function createElement(tag, props, ...children) {
  // Custom component
  if (isFunction(tag)) {
    return tag({ ...props, children });
  }
 
  // Create DOM element with attributes
  const element = addAttributes(document.createElement(tag), props);
 
  // Append children
  children.flat().forEach((child) => {
    const node = isTextNode(child) ? document.createTextNode(child) : child;
 
    if (typeof child === 'function') {
      createEffect(() => createExpression(element, child));
    } else {
      if (node) element.appendChild(node);
    }
  });
 
  return element;
}

The addAttributes function is just a helper to add the attributes to the element, like event listeners, class names, and other attributes.

render.js
function addAttributes(element, props) {
  if (!props) return element;
 
  Object.entries(props).forEach(([k, v]) => {
    // Add event listener
    if (isEvent(k, v)) {
      element.addEventListener(eventName(k), v);
    } // Add class
    else if (k === 'className') {
      const classes = Array.isArray(v) ? v : [v];
      classes.forEach((c) => element.classList.add(c));
    } // Add attribute
    else {
      element.setAttribute(k, v);
    }
  });
 
  return element;
}

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.

render.js
function createElement(tag, props, ...children) {
  // ...
  // Append children
  children.flat().forEach((child) => {
    const node = isTextNode(child) ? document.createTextNode(child) : child;
 
    if (typeof child === "function") {
      createEffect(() => createExpression(element, child));
    }
    // ...
  });
  //...
}

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.

render.js
function createExpression(element, expression) {
  const result = expression();
 
  if (isEmpty(result)) {
    element.innerHtml = "";
    return;
  }
                       
  if (isTextNode(result)) {
    element.textContent = result;
    return;
  }
 
  element.innerHtml = "";
  element.appendChild(result);
}

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:

render.js
function render(element) {
  document.getElementById('root').appendChild(element);
}

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.

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Junk</title>
  </head>
  <body>
    <div id="root"></div>
    <noscript>You need to enable JavaScript to run this app.</noscript>
 
    <script src="./dist/index.js"></script>
  </body>
</html>

Our basic UI library now looks like this:

index.jsx
import { createSignal, createEffect } from "/signals.js";
import { Junk } from "/render.js";
 
function Counter(props) {
  const [count, setCount] = createSignal(props.initialCount);
  const increment = () => setCount(count() + 1);
  const decrement = () => setCount(count() - 1);
 
  createEffect(() => console.log('Count: ', props.count() * 2)});
 
  return (
    <section>
      <h1>The count is: <span>{count()}</span></h1> 
      <Double count={count}/>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </section>
  );
}
 
function Double(props) {
  createEffect(() => console.log('Double: ', props.count() * 2)});
 
  return <span>{props.count() * 2}</span>;
}
 
function App() {
  return (
    <>
      <h1>Hello, world!</h1>
      <Counter initialCount={0} />
    </>
  );
}
 
Junk.render(<App/>);

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: