It’s Time To Talk About Signals

What Signals Are and Why They Matter

The speaker introduces signals as a way to make JavaScript variables reactive, explaining the core concept through a simple counter example. Using pseudo-code with a hypothetical reactive assignment operator, he shows how derived values like isEven and parity could automatically recalculate when their dependencies change, without manual intervention from the developer.

A Brief History of Web Reactivity: From Server-Side to jQuery

The speaker traces the evolution of web reactivity starting from server-side rendering with PHP, describing the 'pull approach' where every state change triggers a full server round-trip. He then covers the jQuery and Ajax era as a middle-ground improvement, and explains why the push to full client-side JavaScript manipulation emerged—primarily for performance, at the cost of predictability.

The Rise of JavaScript Frameworks and Observables

The speaker introduces full application frameworks like Knockout.js as the solution to jQuery-era spaghetti code, noting that Knockout was one of the first to introduce observables—essentially the same concept as modern signals. He traces this pattern through Vue's data and computed properties, Svelte's compiler-driven reactivity and later runes, and Solid's explicit createSignal API, showing a clear convergence across the ecosystem.

React's Deliberate Choice Against Signals

The speaker addresses why React is notably absent from the signals narrative, explaining that React's philosophy deliberately embraces the pull approach—re-rendering entire components on state change for predictability. He outlines React's mitigations including virtual DOM, component tree scoping, memoization via useMemo and useCallback, and the React compiler, framing these as workarounds for the inherent performance cost of the pull approach rather than a fundamental fix.

Building a Minimal Signal Implementation from Scratch

The speaker live-codes a counter demo in plain JavaScript, first showing the manual DOM-mutation approach, then refactoring it to use a custom signal library he builds in about 30 lines. He implements signal state, computed derived values, and effects with automatic subscription tracking, demonstrating how effects self-register as subscribers when they read a signal and are notified on change—making the reactive magic explicit and understandable.

Glitches, Batching, and Efficiency in Real Signal Implementations

The speaker explains the real-world challenges glossed over in the simplified implementation: ordering glitches where computed values recalculate in the wrong sequence, and batching issues where updating multiple signals triggers redundant recalculations. He then describes how modern frameworks use topological trees to recalculate in dependency order, dirty-marking to skip unnecessary recalculations, and lazy evaluation (Solid's push-then-pull approach) to defer computed updates until values are actually needed in the UI.

The TC39 Signals Proposal and the Future of the Web Platform

The speaker connects the convergence of signal implementations across frameworks—an example of what he calls 'carcinization' in front-end development—to the motivation behind the TC39 proposal to make signals a first-class JavaScript primitive. He highlights two key benefits: eliminating redundant framework-level implementations, and enabling browsers and runtimes to deeply optimize for the spec, potentially delivering a performance leap similar to what JIT compilation achieved. He closes with a pointer to the TC39 GitHub repo and its available polyfill.

Thank you. Yes. I wanna talk about signals, and I wanna talk about the customization of the web if we get to that in the end. We'll get to that later. Signals first. Like John said, signals have been around in the mainstream JavaScript ecosystem for a while now, and I think we've hit that awkward spot now where still a lot of people don't really know what they are and what they're used for, but now it's too late to ask. So this talk is specifically for those people, but also for everyone else who just kind of wants to know how signals work, what problem they're solving, and what all the hype is about, especially around the TC39 proposal of making signals part of language.

So thirty minutes is not a lot of time, so I'm actually going to rush through a lot of parts of this, but I hope it still coherently comes all together at the end. And let's start at the very beginning. What are signals? So signals are the idea of making variables in JavaScript reactive. What does that mean? Let's take a look at a very simple example.

So here we have a counter. I'm assigning one to it. And then we have two other variables. One is isEven and one is parity. Those variables are relying on other variables to be calculated, so they're derived from those other variables. IsEven uses counter to check whether the counter is even or not. It's Boolean, and parity gives us a string, even or not, based on this even. So in normal JavaScript, if I reassign the counter value, I also have to recalculate all of those derived values.

Signals or reactive variables are the idea of not having to do that and being able to tell the compiler or the runtime that a variable is derived and needs to be recalculated automatically when any of its dependencies change. So in an imaginative world so this has nothing to do with the Singles proposal yet, but it let's imagine for a moment we had, like, a magical new assignment method in JavaScript.

So instead of just equals, we can go dollar equals, and that tells the compiler, hey, this variable is reactive. So we could still do the same thing. We declare the counter, and then we declare those two derived values. But because we can now tell it that those are reactive values, when we change the counter, it magically happens in the background.

We don't have to manually recalculate things. So that's the core idea of signals. And why do we want that? As JavaScript developers, you might be cringing and saying, like, I've I've been told side effects are bad, we should avoid them. But if we think about it, a lot of what we do as front end developers, is dealing with state, dealing with state changes, and turning set state into a UI.

Oversimplifying here a little bit, but at the end of the day, that boils down a lot of what we do as front end developers. So being able to have reactive variables that then can be reflected or reactive state that can be reflected in the UI is very powerful. At the same time, when I started developing, like, that whole model of state turning into UI, there was a very popular paradigm called model view controller MVC, which followed the same thing.

We were trying to turn state into UI consistently, and the paradigm here was your view should be a pure function of your model, or in more modern terms, your UI should be the result of a pure function of your state. And the pure bit is really important here because it ensures that whenever we feed in the same state, we actually get the same UI out of it, and that leads to predictability.

It's deterministic. This is great for making sure stuff works the way you want it to work. This sounds very much like not what signals do. How did we get from there to signals in modern JavaScript frameworks? To understand that, we need to take another step back and quickly look at the history of reactivity on the web.

And for that, we have to start at the very beginning. I might turn out to be like older here, but when I started, plain HTML was still pretty much a thing. And we can have some kind of server side language. So I'm choosing PHP here, but it could be any other server side language. And this is basically an extreme example of what we just talked about with the Model Y controller.

So we have the state, which in this case comes from the URL. So we take the counter from the URL. And then we use a function, or in this case, a PHP template, to render from that state into an HTML. And in the HTML, have actions in form of links that can change the state. In this case, they change the URL that triggers a new server request, and then the server runs the whole template again, runs the pure function to render the new UI based on the new state. This is what we would call the pull approach.

So every time the state changes, we're pulling the new UI from the server and rendering it. And like I said, this is great for a lot of reasons. This is very deterministic. This is very predictable, easy to reason about, very hard to mess up, and have inconsistencies in your UI. But it comes with a major drawback, and that's performance.

It's obviously not very fast to always, on every state change, go back to the server and fetch the whole thing all over again. So the huge bottleneck here is how do we actually make that fast enough that it leads to a great user experience. So we introduced JavaScript in the era of jQuery and when Ajax just came out. That was kind of the middle ground solution.

So this was still the pull approach, so we would still go to the server to fetch the HTML. But with JavaScript and Ajax, we can now do that from the client and be more specific about it. So instead of having to fetch the whole page, we know, okay, the user clicked this button. We know which part of the AI needs to change, so we only fetch the HTML for that part.

And that way, so we will make the footprint smaller, so it's an approved pull approach, if you will, but it doesn't really solve the whole problem. We're still going to the server, so it's still somewhat laggy. And now we suddenly break that paradigm of a pure function because we have to know which part of the UI changes so we can introduce inconsistencies and stuff like that.

And then from there, for some reason, we went full on JavaScript. So we were like, Okay, what if we didn't go to the survey at all? And the reason for that is simple. Yeah, we lose all of the benefits from the pull. We're now having a push approach, but it's much faster. So what we're doing is purely in JavaScript.

We have the UI. We know the user clicked a button. We now manipulate the DOM and basically write to the DOM directly what we know needs to change. This is much, much faster, but again, you lose all the predictability. And it also kind of leads to why jQuery is still known as like the spaghetti code era of things, unfortunately.

So to unspaghetti fire, we eventually introduced full application frameworks in JavaScript. So I'm calling on Knockout here for a few reasons. For one, it's the first framework that I personally used for, like, full application framework. You might have used something else. Angular came out around the same time, similar frameworks. I'm calling on Knockout also because it's one of the first frameworks, together with Backbone and stuff like that, to introduce the concept of observables. And observables are essentially the same thing as what we now call signals, just an evolution of it.

And we can see in this code, have the k o observable, which defines that observable value, and then we have knockout computed, which defines the derived values that we've seen in the pseudo code example in the beginning. And then all of those frameworks basically come with those three layers. We have state management, which is the view model here. We have templating, which allows us to define the HTML within our framework. And then we have data binding, which connects the state to the templates. So whenever any of the observables change, the framework knows how to update the UI based on that.

And again, that's all we do really as front end developers is connecting those three parts. And this is really powerful because what it really does is it's still a push approach. So underneath the hood, it's pushing to the DOM. But from a developer experience perspective, this templating and data binding gives us a pull developer experience. So as developers, we feel like, okay, we have full control of what's being rendered at any given time.

And this concept stuck. So ever since then, we've seen more and more frameworks come out with kind of the same patterns. And like I said, we've basically taken the concept of observables and have been progressively enhancing them. Basically, the evolution of the concept is visible through those frameworks.

So Vue had the concept of data and computed attributes in your state. Again, same thing. Whenever data changes or computed values change, the data binding to your templates know what to update. Newer versions of Vue, I think there's a concept of refs, which is, again, a similar thing. Then we have Svelte, which also had reactivity baked in.

You just couldn't see it. So this looks very much like plain JavaScript because Svelte, in the beginning, did it underneath the hood through their compiler. Then they realized that's probably a little bit too much magic, so a couple of years later, they introduced what they call runes, which again is the same concept. We have state and we have derived state, and it's now much more explicit. Then SOLID came out in 2019.

SOLID, I believe, is the first framework that started making the term signal popular, or at least they get all the credit for it. So you can see here in the codecs line, have create signal and then create computed as the two paradigms. I could go on and on here. There's a bunch of other frameworks that, end of last year, I think Angular officially moved their architecture to signals, that kind of stuff.

So you can see that hopefully with all of those examples, can see how we changed from pull to push, why we changed from pull to push primarily for those performance reasons. And it's just a popular choice at this stage in modern frameworks. Let's talk about the elephant in the room.

I haven't mentioned React in any of those examples because React famously does not use signals, And there's a lot of talk around that. The reason for that is because fundamentally, the philosophy of React is to embrace the pull approach. They embrace the pull rerender of your application whenever a state changes. So in a React component, whenever that counter state changes, the whole component will rerender. And again, they do that with the argument of, like, that's much more predictable, much less prone to errors.

It comes with their performance downfall. So they try to get around that with a few things. They the component architecture of React allows them to be smart about which subtree of your application needs to rerender because data can only flow top down. So that helps. They use the concept of virtual DOM, so they introduce that concept of virtual DOMs. They try to calculate what actually needs to change in your DOM before they apply those changes to avoid unnecessary computation.

They do a lot of memorization. So if you use React, you know, useMemo and useCallback are flying around everywhere, essentially, and they essentially doubled down on that. So with the introduction of the React compiler, the compiler does a lot of that now underneath the hood, trying to memorize your code to avoid unnecessary rerenders. But it's all basically just trying to fix the issue with a pull approach of the performance of the full rerender.

Now I don't want to go into, like, the whole discussion of, like, which approach is better. Don't have time for that. But I I hope it's clear here that the reason why React is not adopting is because they they're just two very different philosophies that are trying to achieve the same thing. So much so that people started questioning if React is actually a good name for a framework that doesn't embrace reactive variables.

Again, not going to go into that. Cool. I hope you have a rough idea of what signals are, how we got to signals, but how do they actually work? Like, how does that magic happen? To understand that, let's actually try to implement a very dumb, very basic version ourselves in very little time.

So I have this demo application here, which is really just a counter or a button with a counter. When I click on the button, want the counter to increase, and then I print out whether or not the counter is currently even or odd underneath it. And in normal JavaScript, what I would do is basically define a nonclick handler, and in that, I get the current counter from the HTML, then I increase the counter, and then I do that push approach.

So I explicitly go into the DOM and mutate the value one and the parity in a text. If we save that, that's simple enough. But then our project manager, yes, this app has a project manager, comes and says, like, okay, the requirement changed. We need two counters, and we actually wanna add them together.

So we add, like, a sum here. So now we kind of need to do a few things. We need to add, like, a second counter, which is fair enough. Second like, the value of the second counter, we need to have a new sum that we use instead. We also wanna update the sum.

And then we use the new sum for that parity as well. And hopefully, that still works. Cool. But now we have a second button, so we also need to kind of repeat all of that for button two. Cursor is trying to help me here, which is good. Cursor is trying to help me too much.

So you can see we're essentially the same thing. We're pulling the counters from the DOM. We increase the second counter in this case. So there's a few slight differences which make it slightly awkward to actually pull this out into a single function, but it works. Fundamentally, works. It just starts looking slightly awkward. Now we get a third button.

It's not a counter it's not another counter, but it's a button that should increase both counters. So, again, the the the callback is slightly different, so we just make another callback which increases both counters and then updates all of the different HTML DOM nodes.

And again, fundamentally, there's nothing wrong with this approach. It works. But you can hopefully see that this gets unwieldy fairly quickly. So what if instead we had a magic signal implementation that we could import from. And we use those signals to define our state.

So we have the counter one and counter two. So those are our core signals because they are basically what we're setting. And then we have derived values. So the sum and the parity are our computed values. So basically, computed just takes a function that describes how to calculate those computed values. And then we have the concept of effects.

So now an effect is basically just saying, within this effect, rerun this function whenever any signal in this function changes. And we can use that to update the DOM. So that's how a lot of those signal based frameworks work is they have effect handlers for all of your DOM updates.

And in this case, it's pretty straightforward. We have the counter one that should update value one, counter two, value two. You get the gist. And then in the end, we just need to define our actual event listeners again. So if we go, button one, button two, button three. And all we need to do in there now is to actually do the state update.

So we increase counter one for the button one, and we increase both counters for the last button. And this obviously doesn't work yet because we haven't implemented the signals yet, so let's try to do that. But you can already see, not only is it less code, it's much more readable because we've separated the concerns of what is our state, what is the effects that we want to run when the state changes, and what are the actions that actually change our state.

So it's pretty neat. Cool. Let's implement signals. And again, cursor is going a bit over the line here. But basically, a signal has two two things that we saw before. It has a getter, which returns the value of the signal, and a setter, which sets the new value of the signal.

And then we have computed values and effects. Computed values only have a getter because by definition, those derived values, we would never set a specific value to those. We're just describing how they are calculated, so we only want to get those and then run the function essentially, and effects, which also are just functions that we're running.

I don't think I set the disclaimer in the beginning, so I say it now. Huge disclaimer. This is a very simplified dumb version of signals. Right? So don't expect this to be like the implementation of the actual spec or anything. That would be crazy. But this is just to show you the basics of what happens underneath the hood. So if we save this, this still doesn't do anything because the magic parts of signals is still not there, which is the how do effects know when your signals update.

Right? Like, how do signals tell the effects that they updated or however way it works? So for that, we basically just need to implement a kind of like simplified version of an event system. So it also helps to have that mental model that signals are really just small event emitters that tell everyone else whenever they change.

For that, we need to keep track of when effects are running. We store that here, and then we reset it afterwards. So now that we know when effects are running, we can use that knowledge whenever a signal is being read. Because if we know that's within an effect function, we can just add that function as a subscriber of our signals.

Then whenever we change the value, we notify all the subscribers. Again, this is overly simplified, but this does the trick. It does the magic auto subscription whenever our signal is read, and then the broadcasting whenever the signal is changed. And hopefully, this just works.

So in 30 lines of JavaScript, we basically created that magic foundation of observable variables in our app, which I think is pretty cool. But again, I've mentioned it a couple of times. This was a simplified version of it. So why I'm saying that is because I glossed over a few challenges that come with the implementation of signals.

And I wanna call out a few of them here just to paint the picture of, like, what is actually happening in modern frameworks and how signals evolved from what we had in Knockout to what we have in frameworks like Solid these days. And there's two things that I specifically want to call out. The first one is glitches.

When you deal with signals, have to deal with glitches. Glitches basically mean you can get into wrong states if you do things slightly wrong. One of the best examples is recalculating your computed values in the wrong order. So in this example, we have a counter and we have an increased derived value, which is always the counter plus one.

And then we have another derived value which just compares the increased against the counter. So logically, the increased value should always be larger because by definition, it's the counter plus one. So is greater should always be true. But if we just recalculate the computed values whenever the counter changes in an arbitrary order, we might actually calculate is greater first.

Say counter is increased to one and is greater is the next one to be recalculated, it actually then compares increase, which is still one from the previous value, against the counter, which is also now one. So is greater actually becomes false, which is the glitch. So it's really important with signals, with reactive values, to recalculate them in the right order based on the dependencies.

That's why all modern implementations basically have a topological tree that they follow when they recalculate values on state changes. The other popular glitch is coming when you change multiple values at the same time. So in this case, have two counters and a sum. Sum is calculated from counter one plus counter two, and we increase both. And if we have an effect that logs out whenever the sum changes, we will actually see that that sum changes twice.

That this is because signals inherently are still synchronous. So when we change counter one and counter two, counter one changes first, that recalculates the sum, then counter two changes, that recalculates the sum. Modern frameworks, again, have dealt with that, so there's smart batching around certain events. When you change multiple multiple of your signals, they will all be batched into one kind of recalculation approach.

We can actually see that in action as well if we have we're slightly running out of time. But if we just put an effect here, that console looks out. Okay. That was a bad idea.

And some get. Then we should hopefully see here. If we refresh you can see that it runs basically twice because our simplified implementation doesn't deal with the batching. Well, so it actually runs on every change of the state.

The other thing that modern frameworks optimize when it comes to signals is trying to be smarter around the efficiency. So the compute time, the memory time, especially if you have a large scale application with a lot of state, you want to avoid unnecessarily running a lot of those computations. So there's basically two things that frameworks take advantage of with that topological tree, and the first one is marking the nodes in the tree as dirty. So whenever the counter changes in this example, all the computed values in the tree that rely on the counter will immediately switch to being dirty because the counter is dirty.

And then the framework will go through basically each of the nodes and do the recalculations. But before the recalculation happens, the node can check if any of this if all of their dependencies or any of the dependencies are still dirty or if all of the dependencies are actually clean and can skip the recalculation. So to put an example on this, we have this counter, we have iseven and parity again.

We set the counter to three, so iseven needs to recalculate because it depends on counter. So it recalculates. It is now false. It was true before. So it is different, so it stays sturdy. Parity, therefore, also needs to recalculate because it depends on isEven and that is sturdy, so all values recalculate.

But if we now set the counter to seven, isEven still needs to recalculate, again, because it relies on counter, but it now is false. It was false before, so it marks itself as clean. And now when parity recalculates before it runs the function, it will check, it will see that all of its dependencies are clean, and therefore it will just skip the recalculation.

This is a very simple example, but at scale, this can make a massive difference for your performance and your memory and compute usage. The other thing that you can do with modern signals or that modern signals try to do is to be lazy. So this is kind of what Solid started calling the push then pull approach rather than just a push.

So whenever a signal changes, it marks everything as dirty, but the computed values are not recalculated immediately. They're recalculated when they're actually used somewhere in the UI. So stuff that has already been, like, removed from from the from the UI or whatever doesn't run if it's not there anymore. So, again, this is just to show there's a lot of evolution that happened over the last ten, fifteen years that got us to the point where signals are today, which brings us to the last point, the TC39 proposal and the promised customization of the web.

The whole idea is that we've seen so many front end frameworks pick up signals to solve their problem of reactivity, and they've all kind of started in different ways and different implementations, but they've all come together and the evolution and convergence of all of these implementations is very visible. So this is what's been called the calcinization of the web, because calcinization from biology, for those who don't know, it's the idea that nature basically came up with a bunch of different crustaceans that then evolved all into crabs, which makes a lot of people think crabs are the best form of life, apparently.

But the idea is just that convergence of, like, if we find the right solutions, all of those different implementations will eventually end up in the same direction. So we see that with a lot of stuff in front end actually, but specifically with signals in modern front end frameworks. The idea of the proposal is, as John said, to actually pull signals into JavaScript as a first class citizen as a new primitive that we can use.

And you can see here that that's an early version of the proposed API that they're thinking about, and it looks very similar to what we've seen throughout this talk. So we have the signal state. We have signal computed, which is derived values. And there is a concept of effects, but you would probably never really use them directly.

This is stuff that would be hidden inside the framework that you use that then does smart stuff with templating and so on. So it's very similar to to all of those implementations, but there's core benefits to actually making it part of the language. For one, the obvious one, let's not reinvent the wheel with every framework. Let's actually pull out that heavy lifting into the core language so that the frameworks can focus on other stuff that that really matters. The second one for me is probably the bigger one.

Once signals are part of the JavaScript spec, all of the runtimes of browsers and and server runtimes can actually start optimizing for that spec. So we've seen that before with, like, just in time compilation and that kind of stuff, where we had a huge spike in performance both in the browser and on the server.

And I think if signals make it into the spec, we will see the same thing, where performance, memory usage, all that kind of stuff just gets massively optimized, because now we have a spec that we can work from. So if I sparked any interest on any of this, definitely check out the GitHub Reaper for the TC39 proposal.

They also have a polyfill that you can use today, and then give feedback on the developer experience and the actual functionality behind it. This is a link and a QR code to the slides. There's also some more resources in those slides, some links to my socials. But, yeah, if you're if you wanna talk about signals, talk about JavaScript, or anything else, please hit me up.

I'm here today and tomorrow. But, yeah, that's thank you very much.

let counter = 1;
const isEven $= counter % 2 === 0;
const parity $= isEven ? "even" : "odd";

The final comparison contrasts ordinary variables, which require manually recalculating dependent values after the counter changes, with reactive declarations for isEven and parity.

let counter = 1;
const isEven $= counter % 2 === 0;
const parity $= isEven ? "even" : "odd";

// Changing `counter` triggers re-calc
counter = 6;
// → isEven = true
// → parity = "even"

The reactive example shows that assigning 6 to counter automatically updates both dependent values.

Your UI is the result of a pure function of the State.

For a given state, no matter how you arrived at that state, the output of the system is always the same.

A flow diagram runs from State through View to UI. A feedback path returns from UI to State, representing actions that change state and produce a new deterministic UI.

A brief history of reactivity on the web

1995 – HTML and server-side code, e.g. PHP

“Pull”

<?php
  $counter = $_GET["counter"];
?>

<p>Counter: <?php echo $counter; ?></p>
<p>Parity: <?php echo $counter % 2 ? "odd" : "even"; ?></p>

A Server → DOM → Action cycle shows that each action requests a new DOM from the server based on changed state.

2006 – jQuery and AJAX

Improved “pull”

function update(val) {
  $content.attr("data-count", val);
  $.ajax(serverUrl, { val }).done((html) => {
    $content.html(html);
  });
}

The diagram shows an action requesting server-rendered HTML for one specific portion of the DOM rather than replacing the entire page.

2006 – jQuery

“Push”

function update(val) {
  const parity = val % 2 ? "odd" : "even";
  $content.attr("data-count", val);
  $counter.text(val);
  $parity.text(parity);
}

The push-model diagram shows an action directly mutating only the required DOM elements, without requesting replacement HTML from the server.

2010 – Knockout

“Push”

function ViewModel() {
  this.counter = ko.observable(0);
  this.parity = ko.computed(() =>
    this.counter() % 2 ? "odd" : "even"
  );
}
ko.applyBindings(new ViewModel());

Bindings connect observable and computed ViewModel values to the interface, while the app renderer directly updates the necessary DOM region after an action.

2014 – Vue

“Push”

var vm = new Vue({
  data: { counter: 0 },
  computed: {
    parity: function () {
      return this.counter % 2 ? "odd" : "even";
    }
  }
});

Vue templates bind counter and parity output to reactive data and computed state; the app renderer pushes targeted changes into the DOM.

2016 – Svelte

“Push”

let counter = $state(0);
const parity = $derived(
  counter % 2 ? "odd" : "even"
);

Runes were introduced later.

The build changes Svelte’s initially implicit, compiler-managed reactivity into explicit $state and $derived declarations. In both versions, updates are pushed to the required DOM region.

2019 – Solid

“Push”

const [counter, setCounter] = createSignal(0);
const parity = createComputed(() =>
  counter() % 2 ? "odd" : "even"
);

Solid represents state with a signal and derives parity with a computed value, allowing the app renderer to push precise updates to the DOM.

What about React?

2013 – React

“Pull”

const [counter, setCounter] = useState(0);
const parity = useMemo(
  () => counter % 2 ? "odd" : "even"
);

The pull-model diagram shows that any state change triggers the app renderer to re-render the component’s subtree before updating the DOM.

“We might add a signals-like primitive to React but I don’t think it’s a great way to write UI code. It’s great for performance. But I prefer React’s model where you pretend the whole thing is recreated every time. Our plan is to use a compiler to achieve comparable performance.”

— Andrew Clark, February 18, 2023

“‘React’ is a terrible name for @reactjs.”

— John Lindquist, March 24, 2019

Building a basic signal implementation

const counter1 = signal(0);
const counter2 = signal(0);
const sum = computed(() => counter1.get() + counter2.get());
const parity = computed(() =>
  sum.get() % 2 === 0 ? "even" : "odd"
);

effect(() => window.sum.innerText = sum.get());
effect(() => window.parity.innerText = parity.get());

A live-coded counter app begins with one button whose click handler manually reads and mutates DOM values. Adding a second counter, a sum, parity, and an “Increment both” action causes duplicated update logic. The code is then refactored around signal, computed, and effect: counters hold reactive state, sum and parity become derived values, and effects synchronize each output with the DOM. A small signal.js module supplies getters and setters, lazy computed getters, and effects. The finished demo correctly updates two independent counters, their sum, and even/odd parity; the verified final state is Counter 1: 5, Counter 2: 4, Sum: 9, Parity: odd.

How does it work?

There are still challenges

const counter = signal(0);
const increased = computed(() =>
  counter.get() + 1);
const isGreater = computed(() =>
  increased.get() > counter.get());

counter.set(1);

// Depending on the order the computed
// values are recalculated, you can end up
// in a glitch state where isGreater is false.

A state sequence demonstrates a reactive glitch. Initially, counter is 0, increased is 1, and isGreater is true. After the counter becomes 1, recalculating isGreater before updating increased temporarily compares 1 > 1 and produces false; only afterward does increased become 2. The diagram emphasizes that recalculation order matters.

const counter1 = signal(0);
const counter2 = signal(0);
const sum = computed(() =>
  counter1.get() + counter2.get());

const increaseBoth = (amount) => {
  counter1.set(counter1.get() + amount);
  counter2.set(counter2.get() + amount);
}

effect(() => console.log(`Sum: ${sum.get()}`));

increaseBoth(2);
// Sum: 2
// Sum: 4

A dependency sequence illustrates a reactive glitch: the sum begins at 0, recalculates to the intermediate value 1 after only the first counter changes, then recalculates to 2 after the second counter changes. The annotation explains that unbatched changes expose intermediate states.

const counter1 = signal(0);
const counter2 = signal(0);
const sum = computed(() => counter1.get() + counter2.get());
const parity = computed(() => sum.get() % 2 === 0 ? "even" : "odd");

effect(() => console.log(sum.get()));

window.buttonboth.onclick = () => {
  counter1.set(counter1.get() + 1);
  counter2.set(counter2.get() + 1);
};

In a live-coding demonstration, a logging effect is added to a two-counter application. Clicking “Increment both” changes both signals from 0 to 1; the page finishes with a sum of 2 and even parity, while the console records 0, 1, and 2. This verifies that the simplified implementation runs the effect after each individual state change and exposes the intermediate sum because it does not batch the updates.

const counter1 = signal(0);
const counter2 = signal(0);
const sum = computed(() =>
  counter1.get() + counter2.get());

const increaseBoth = (amount) => {
  counter1.set(counter1.get() + amount);
  counter2.set(counter2.get() + amount);
}

effect(() => console.log(`Sum: ${sum.get()}`));

increaseBoth(2);
// Sum: 2
// Sum: 4

A dependency sequence shows the sum recalculating from 0 to an intermediate value of 1 after the first counter update, then to 2 after the second. The annotation identifies this observable intermediate state as a glitch caused by unbatched changes.

const counter = signal(0);
const isEven = computed(() =>
  counter.get() % 2 === 0);
const parity = computed(() =>
  isEven.get() ? "even" : "odd");

counter.set(3);
// isEven = false
// parity = "odd"

counter.set(7);
// isEven = false
// parity = "odd"

A dependency diagram follows the counter, isEven, and parity values. When the counter changes from 3 to 7, the counter becomes dirty, but isEven remains false and clean; because parity’s dependency has not become dirty, its recalculation can be skipped and it remains “odd.”

The TC39 Proposal

Frontend frameworks have been evolving and converging towards the same solution for the same problem

We’re all becoming crabs.

An illustration depicts crab mascots representing React, Vue, and Angular together on a beach, using convergent evolution as a metaphor for frameworks independently arriving at signals.

const counter = new Signal.State(0);
const parity = new Signal.Computed(
  () => counter.get() % 2 ? "odd" : "even"
);

const decrease = () => counter.set(counter.get() - 1);
const increase = () => counter.set(counter.get() + 1);

// A library or framework defines effects using low-level
// primitives
declare function effect(cb: () => void): (() => void);

effect(() => window.counter.innerHTML = counter.get());
effect(() => window.parity.innerHTML = parity.get());

TC39 proposal for signals

Link to the slides

Web Directions Code 2025 slides

A QR code provides access to the presentation slides.

Technologies & Tools

  • Ajax
  • Angular
  • Backbone.js
  • Cursor
  • jQuery
  • Knockout.js
  • PHP
  • React
  • React Compiler
  • SolidJS
  • Svelte
  • Svelte Runes
  • useCallback
  • useMemo
  • Vue

Standards & Specs

  • TC39 Signals proposal

Concepts & Methods

  • Carcinisation
  • Just-in-time compilation
  • MVC
  • Observables
  • Push-pull reactivity
  • Signal batching
  • Topological sort
  • Virtual DOM

Organisations & Products

  • TC39