Signals: easy concept, hard to implement... except it's not

Taeber Rapczak <taeber@rapczak.com>

Monday 2 March 2026

Been a few years since I wrote anything. This weekend was disturbing and I just felt like writing, I guess.

Speaking of a few years ago, I think that was about the time I first heard folks talking about signals and how they were the future of web development.

"It's a simple concept, but powerful." They proceeded to describe a straightforward data-binding scenario where a change in the data model would automatically update the UI.

Having worked on the web for several decades and written plenty of XAML code (I know, cool flex), I immediately recognized this old pattern and thought, didn't React save us from this? Didn't we learn way back in 2013 that unidirectional data flow is the answer?

In fact, there were already libraries and frameworks that used two-way data binding like Knockout and RxJS was also released around that time but, if my memory serves correctly, most of the web was using jQuery.

As a minor aside, 2013 was 13 years ago. It's entirely possible that the Signals people I've mentioned might simply not have been developing in 2013. I mean, developing sure, but writing code? Your 23 year old coworker was 10. This isn't meant to disparage younger devs; rather, web development is old enough that there is actual, literal history to it.

New practitioners might benefit from learning about it. (The history. Not the parties.) Like, with some academic rigor.

Fast forward a few years, and I heard that Signals have finally landed. That it took several years to get right because of... reasons.

Easy Concept

Now, this isn't an "Introduction to Signals" piece, but even so, let's start with how the concept is explained in the wild. As it isn't specific to a framework, I'll arbitrarily use Angular's explanation:

Angular Signals Overview

A signal is a wrapper around a value that notifies interested consumers when that value changes.

const count = signal(0);
// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());

// To change the value of a writable signal...
count.set(3);

The rest of that overview explains the various ways to "react" to changes using computed() and effect() and so on.

OK, let's see what Preact has to say:

Preact Signals

import { signal } from '@preact/signals';

// Create a signal that can be subscribed to:
const count = signal(0);

function Counter() {
  // Accessing .value in a component automatically re-renders when it changes:
  const value = count.value;

  const increment = () => {
    // A signal is updated by assigning to the `.value` property:
    count.value++;
  };

  return (
    <div>
      <p>Count: {value}</p>
      <button onClick={increment}>click me</button>
    </div>
  );
}

Easy peasy.

Hard to Implement

Recall that the Signals chatter was a few years ago. I pretty much dismissed it as hype. Sort of recently, I heard it landed in multiple frameworks, but very recently the push at work to update things is real.

Years. Why did it take years?

Well, I won't answer that. After all, I'm not a journalist investigating real reasons. I'm just another snarky, sarcastic writer who thinks they know better and feels they have something to say.

Completely ignoring why, I want to point out how they were implemented. Here are some of the directories in Angular that I could find:

The documentation explains their producer and consumer concept called ReactiveNode, the Dependency Graph they use to track liveness, and much more.

Wow. No wonder it took years!

Preact? Looks like 1-ish file. 1000 lines of TypeScript. Maybe a few more if you count their @preact/react monkey-patching library. Interesting.

Except It's Not

Angular took a few years, but Preact implemented it in about 6 months (according to their GitHub repos). Angular has tens of files and thousands of lines of code, but Preact has one or two files and a thousand lines.

How hard is it? Pretty hard if you take Angular's approach and re-invent things as opposed to using the already existing features of the runtime like Preact smartly did. WAIT! What is this, Preact? Why aren't you using the built-in, existing, reactivity mechanism that's available in every JavaScript runtime either?

What built-in mechanism am I talking about, you ask? Let me answer that and explain my understanding of what we're trying to achieve concisely and precisely using glorious JSDoc:

/**
 * @template T
 * @typedef {
 *   { ()      : T    } &
 *   { (val: T): void } &
 *   EventTarget  } Signal<T>
 */

In case you didn't catch that, EventTarget exists.

Furthermore, according to Mozilla, it's Baseline Widely available. Been that way since 2015. (That's 11 years ago, for those playing along at home.) Evenfurthermore, I can personally attest that it has existed long before it was "Baseline". I'm talking dot-com era. Oh, you might have had to spell addEventListener as attachEvent, but it was there.

How to implement signals in JavaScript:

function signal(value) {
  const target = new EventTarget()

  const sig = function (newValue) {
    if (newValue === undefined)
      return value

    if (value !== newValue) {
      value = newValue
      target.dispatchEvent(new CustomEvent("change"))
    }
  }

  sig.addEventListener    = target.addEventListener.bind(target)
  sig.removeEventListener = target.removeEventListener.bind(target)
  sig.dispatchEvent       = target.dispatchEvent.bind(target)

  sig.toString = function () { return `${value}` }

  return sig
}

Remember, signals were described as getter-functions that you can subscribe to, so this code, literally create a function that is/has an EventTarget. It's a getter, you can subscribe to it, and it's even a setter because this is JavaScript:

const name = signal("")

name.addEventListener("change",
  () => console.log(`Hello, ${name()}!`))

name("world")
name("you")

/* Output:
Hello, world!
Hello, you! */

What about computed? Sounds like a signal that takes a signal as its value. Easy:

function computed(base, transform) {
  const sig = signal(transform(base()))
  base.addEventListener("change", function () {
    sig(transform(base()))
  })
  return sig
}

computed(name, val =>
  val === "me"
    ? "Oh good, it's you!"
    : `Who are you, ${val}?`)

What about effect? You got me. You can't implicitly subscribe to an EventTarget-based Signal just by referencing it, but I prefer less magical code anyway.

Your event handlers can do whatever: write to the Console, file your taxes, and, yes, even update the DOM to re-render a portion of your page.

Just don't try to update the signal from within the change handler, of course.

Conclusion

I dunno. Not even sure we should be using signals. Just thought I'd share some thoughts. Thanks for reading.

Here's my code: github.com/taeber/signal.js.