DEV Community

Cover image for Implementing Signals from Scratch
RATIU5
RATIU5

Posted on

Implementing Signals from Scratch

What Are Signals?

Recently, the JavaScript community has been buzzing about signals. Their rise in popularity can be traced back to Solid.js, which drew inspiration from Knockout.js' Observables to craft their version of signals. Not long after, prominent frameworks like Preact, Angular, and Qwik integrated signals into their core. Vue 3 introduced its distinctive take on signals with ref and reactive (although they are not signals in the same context as Solid.js' signals), while Svelte 5 unveiled the Svelte Runes which is fundamentally built on this type of reactivity. For the purpose of this article, I'll use the term "signals" to describe these reactive systems. With that said, what the heck are signals?

Signals are basic units of data that can automatically alert functions or computations when the data they hold changes. This alerting capability allows parts of a system to automatically and immediately update when the data changes, making the system feel dynamic and real-time. The problem this solves is updating something visually when some data changes behind-the-scenes.

When data changes, a function is triggered to update a specific element on the DOM. Solid.js achieves this with fine-grained reactivity. This ensures that your code directly updates only the specified value, avoiding unnecessary side effects or redundant re-renders of other DOM elements. With a defined reactive system in place, you can build large-scale and maintainable web applications with ease.

How Do Signals Work?

Let's look at how signals work under-the-hood. I will be referring mostly to Solid's functional approach of signals, although a class-based solution wouldn't be too different. The signal function we will be creating today isn't going to be as performant or feature-full as with many frameworks, but should serve instead as a starting point to understanding signals at a low level.

Functions and Closures

Before we look at signals, it's important to have a grasp of how JavaScript handles functions. Let's dive deep into how those work, starting with the following code:

function createSignal() {

}
Enter fullscreen mode Exit fullscreen mode

Let's dive in. The function createSignal is stored in JavaScript's global memory. Simple enough, right?

Next, we'll embed a variable within our function and return another function to retrieve this value.

function createSignal() {
  let value = "Hello, World";
  return function() {
    return value;
  }
}
Enter fullscreen mode Exit fullscreen mode

Our function now gets a little more complex, and shows the creative inner workings of JavaScript. By invoking:

let signal = createSignal();
signal();
Enter fullscreen mode Exit fullscreen mode

We initiate a fresh execution context for createSignal. Within that context, the string "Hello, World" is assigned to our context's memory under the value label. When we return the new function, a closure is created that holds the value data, and is stored alongside the returned function. This allows us to have persistent storage of our value across execution contexts.

Upon invoking the returned function, JavaScript sets up a new execution environment. Since it doesn't immediately spot the value variable, it consults the closure, locates value, and duly returns it.

Now, let's modify our function. We will now return an object with a setter function and the value. Then we will add a parameter that receives a default argument for our value as well.

function createSignal(initialValue) {
  let value = initialValue;
  return {
    value,
    set: (v) => { value = v; },
  }
}
Enter fullscreen mode Exit fullscreen mode

We have one problem. Because we are returning the value variable within our object, it remains unchanged even after calling our set function. This happens because the value we pass to the object is a copy of the value at the point in time where we return the object from the function. Hence we need to write a dedicated getter function for the value.

function createSignal(initialValue) {
  let value = initialValue;
  return {
    get: () => { return value; },
    set: (v) => { value = v; },
  }
}
Enter fullscreen mode Exit fullscreen mode

It's coming together! Let's try using it.

let signal = createSignal(10);
console.log(signal.get()); // 10
signal.set(20);
console.log(signal.get()); // 20
Enter fullscreen mode Exit fullscreen mode

One thing that stands out is the need to call a set and get function each time we read or write to the value variable. Let's improve this by using JavaScript's get and set functions.

function createSignal(initialValue) {
  let _value = initialValue;
  return {
    get value() { return _value; },
    set value(v) { _value = v; },
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we can use our function as such:

let signal = createSignal(10);
console.log(signal.value); // 10
signal.value = 20;
console.log(signal.value); // 20
Enter fullscreen mode Exit fullscreen mode

A bit more readable, eh? We still have one problem: it's not reactive. No "effect" happens aside of the _value changing states when we call the set function. This is where we will create a subscriber.

Subscribers

A subscriber will "subscribe" a function to run some code whenever our _value changes. To do this, we will be making use of our get function.

function createSignal(initialValue) {
  let _value = initialValue;

  function notify() {

  }

  return {
    get value() { return _value; },
    set value(v) { 
      _value = v;
      notify();
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

What's happening here? Whenever the set function is called (aka. we reassign the value signal.value = "hello";), we will run a function. This function will then call the subscriber function... which means we also need a subscribe function as part of our return. While were at it, let's accommodate for multiple subscribers and then call them within our notify function.

function createSignal(initialValue) {
  let _value = initialValue;
  let subscribers = [];

  function notify() {
    for (let subscriber of subscribers) {
      subscriber(_value);
    }
  }

  return {
    get value() { return _value; },
    set value(v) { 
      _value = v;
      notify();
    },
    subscribe: (subscriber) => {
      subscribers.push(subscriber);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Finished Signal

And with that, we have a (very) basic signal! Let's see how we will use it:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Signals from Scratch</title>
</head>
<body>
  <span id="mySpan"></span>
  <script>
    function createSignal(initialValue) {
      let _value = initialValue;
      let subscribers = [];

      function notify() {
        for (let subscriber of subscribers) {
          subscriber(_value);
        }
      }

      return {
        get value() { return _value; },
        set value(v) {
          _value = v;
          notify();
        },
        subscribe: (subscriber) => {
          subscribers.push(subscriber);
        }
      }
    }

    const mySignal = createSignal("");
    mySignal.subscribe((value) => {
      document.getElementById("mySpan").innerHTML = value;
    });

    mySignal.value = "Hello World!";
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

What's happening here is we defined a variable mySignal to hold our reactive signal. We call the subscribe method on our returned and bind a function that will be called whenever our value setter is called which will in turn update the DOM. Now whenever we set the value of our signal, our subscriber is notified and the DOM is updated!

At the root, this is what's happening with signals. Of course, frameworks implement lots of additional features like derives and effects. In Solid's case, they make use of additional improvements to performance with the use of subscriber cleanups and a compilation step to check where you use the getter within your JSX and create the fine-grained update code from that.

That's it! If you notice any mistakes, please let me know in a comment and I will do my best to fix it. Feel free to give your thoughts and improvements to this as well!

Top comments (23)

Collapse
 
thejaredwilcurt profile image
The Jared Wilcurt

Q: "What are signals?"
A: A meaningful step forward for frameworks to get closer to catching up with what Vue was doing in 2017, though with manual effort instead of it being handled automatically. Also known as "a waste of your time doing something the framework should handle for you for free".

Collapse
 
dannyengelman profile image
Danny Engelman • Edited

If we are going to play the When game, we will end up .. or start.. somewhere in the previous century. Semaphores...anyone?

Collapse
 
artydev profile image
artydev

And before Vue there was (S)[github.com/adamhaile/S] :-)

Collapse
 
coolcucumbercat profile image
coolCucumber-cat

Are you saying Vue doesn't handle it automatically? You really can't get less automatic than what Vue does: ห‹const count = ref(0); ref.value = 1ห‹. How can you make it more automatic?

Collapse
 
thejaredwilcurt profile image
The Jared Wilcurt

No, I am saying Vue has been handling reactivity for you since day one. Especially the features specific to Vue 2 (hence 2017 being mentioned). And I am referring to the declarative Options API. You are referencing the lower-level API that hooks into the reactivity engine (Composition API). That approach is useful when handling niche edge cases or building a library. However, the Options API is specifically designed to solve the common problems of writing applications.

<template>
  Display reactive values: {{ count }} {{ doubledCount }}
  <button @click="count++">Modify reactive value from template</button>
  <button @click="increment">Modify reactive value in methods section</button>
</template>
<script>
export default {
  data: function () {
    return {
      count: 0
    };
  },
  methods: {
    increment: function () {
      this.count++;
    }
  },
  computed: {
    doubledCount: function () {
      return this.count * 2;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

JSFiddle of above

Notice how I don't need to do anything more than just put the code in the correct section and the framework handles everything. Reactive state is free. Until the browser mentions that value was changed, no reactive code is executed, it is the same from a memory or CPU stand point as if it were static. Derived state is also free, it only re-computes the computed data when something it is dependent on reactively changes, and then the output is cached, so it executes the least amount possible. Note that you never need to babysit any of this, or explicitly spell things out, like what is/isn't reactive, or what to watch, or when to re-render the DOM. The framework can handle all of this automatically, and it can do a better job than you can doing it by hand. This is why Vue apps are 2-6 times faster than React apps. They are not the same.

Notice how there are specific sections for your code. It is organized by default, and the features of the framework are built in to this organizational structure. I cannot overstate how important this is. Every frontend framework should be copying this approach. You can go to any component, in any codebase in the world and instantly know where everything is. There is no need to invent and document and train every new person on "how things are done" or "where things go" in your codebase. You can take your intern, point them at the official Vue docs, and say "do it that way". Then come back 20 minutes later and they are already doing a commit (true story).

You will inevitably need to create bespoke patterns for your projects that are unique to that specific project's purpose. So let the framework solve these common problems for you, so you can focus on the parts unique to the app you are trying to build. Vue gets out of your way and lets you make things quickly, and in a consistent, testable, scalable, modular, performant, and organized-by-default way. No other framework even comes close. And we didn't even talk about Pinia.... it's so good, I'm convinced it was sent back in time from the future.

Thread Thread
 
coolcucumbercat profile image
coolCucumber-cat

Vue3 > Vue2. If you just follow conventions you also know exactly where code is going to be. And the structure of Vue2 is the exact reason why the Composition API was created, because that structure sucks.

Thread Thread
 
thejaredwilcurt profile image
The Jared Wilcurt • Edited

There are no conventions. You must invent your own conventions and organizational structure. And no matter how organized you think you are, you aren't. And even if you are, the second you leave the project no one will be there to enforce your conventions. The framework comes with a built in organizational pattern that rewards you for using it.

What about the Options API's approach "sucks"? The only valid critique is Mixins, and they're not even that bad unless you're doing nested mixins, but even then an editor-level solution would solve that. But until there is one, Mixins being pointed to is valid. But that's a pretty minor downside compared to the massive downsides of Composition API. It's just another way of making spaghetti.

Thread Thread
 
coolcucumbercat profile image
coolCucumber-cat

It's not hard to come up with a logical convention. It's just common sense where you put things. For example, you could follow the Options API convention.

The Composition API is structured in a functional, non-spaghetti way. I don't know how to explain this, even if it seems logical in my head, but everything is completely separate, unless you make it be. You can only access the variables you can actually see, you can't mess with other things. There are no accidental side effects, it's all pure. It just works like normal JS and easy to type with TS. With the Options API it feels all jumbled up like spaghetti and all proprietary and connected to Vue. You can't just something how you want to. For example, composables aren't something Vue invented, they're a pattern you can use. You can do things how you want and it's super customisable.

Hopefully I explained it well, but I probably didn't. If you want to know what I mean, extract all the logic for a to-do app into a different file, but still so that you can do things in the file with the template. With Composition API, you can make a composable that holds the state and returns all the state and functions to interact with the state. That just doesn't work with Options API.

Collapse
 
efpage profile image
Eckehard

What precisely distinguisches "States" from "Signals"? Sounds as if they are more or less the same - despite the different name.

Reflecting state changes in the DOM is handy, but what, if your logical context needs a more complex interaction than just updating a dom element? Assume you need to fetch a value from a database, if a "signal" changes it's value?

And how do you avoid update loops? A triggers B, B trigger A and so on... A Dom element might have an .onChange-event, that cahanges a signal...

Collapse
 
ratiu5 profile image
RATIU5

Signals are states with a joined effect, or function that runs when it changes. This effect that runs doesn't just have to update the DOM. Frameworks like Solid will detect where the signal is being used within the JSX and create the necessary DOM update code.

The current design doesn't take into account if you trigger A from B, and B from A. You'll need to introduce either a limit to track the number of subscriber calls and stop at a certain number, or you could set up a lock so when one subscription is running, other ones won't be called. In most cases, major refactoring will be needed.

Collapse
 
efpage profile image
Eckehard • Edited

But Isnยดt this what all states do? See Van States or React State:

React components has a built-in state object.
The state object is where you store property values that belong to the component.
When the state object changes, the component re-renders.

Most modern programming languages know getters and setters for at least 30 years see Mutuator method, so it is not a new concept. But instead invoking a function when a state has changed, a setter function is invoked before a state has changed. This gives you more control over the process (e.g. limiting the value range of a variable change) and makes it unnecessary to store the value of the state object before it was changed (which some state libraries provide).

Thread Thread
 
ratiu5 profile image
RATIU5

A react state is different than just a plain state. A state on it's own is just data at a certain point in time. React has type of state is is bound to an effect for re-renders. I don't know about Van states to give any input there.
I could be wrong but I welcome any correction.

Thread Thread
 
efpage profile image
Eckehard • Edited

Van states are more or less variables that can be bound to any DOM property. So, a "plain state" ist more or less a variable in that context. An "immutable state" would just be a constant. see Immutable Object

As far as I see, the concept of "immutable state" makes only sense in platforms like React, where you need to ensure, that a state does not change during a page rendering.

So, itยดs interesting to see, how much effort it takes to implement signals in the context of React, which otherwise would be a very simple interaction...

Collapse
 
qwertme profile image
Eric di Domenico • Edited

You know you are old when you read about the latest trend and say to yourself: "Isn't this exactly the same as [NSERT TECH] that I was doing 15 years ago?"

In this particular case it was RETE algorithm, there are several implementations for different programming languages. Invented in 1979 apparently.

Anyway, you define rules that trigger on certain states. When you modify the state the rules trigger in cascade. It's pretty neat and very useful in solving business rules.

Collapse
 
webjose profile image
Josรฉ Pablo Ramรญrez Vargas

[INSERT TECH] = Knockout.js

Collapse
 
tailcall profile image
Maria Zaitseva

Amazing read! Thanks for posting.

Collapse
 
bwca profile image
Volodymyr Yepishev

Thanks for the article! Somehow it reminds me of rxjs Subject :)

Collapse
 
hasanelsherbiny profile image
Hasan Elsherbiny

good article

Collapse
 
oli8 profile image
Olivier

cool rundown, thank you

Collapse
 
sabbir2609 profile image
sabbir2609
Collapse
 
samuelthng profile image
Sam

What's different between signals and a regular old pub-sub pattern?

Collapse
 
artxe2 profile image
Yeom suyun

I am curious about what React 19 will look like.
In various meanings.