If you’re into web development, you may have heard about the buzz around Signals lately. But what exactly are Signals, and how do they work? And, more importantly, how do they differ from the lifecycle-oriented frameworks we use today?
At its core, Signals is an event-driven programming paradigm that enables developers to create custom events and event listeners within their JavaScript code. This means that they can trigger callbacks or functions when certain events occur within their application, such as user input or data changes. What’s great about Signals is that it allows for complete decoupling between logic and UI, making code more modular and easier to maintain.
If you’re into web development, you may have heard about the buzz around Signals lately. But what exactly are Signals, and how do they work? And, more importantly, how do they differ from the lifecycle-oriented frameworks we use today?
At its core, Signals is an event-driven programming paradigm that enables developers to create custom events and event listeners within their JavaScript code. This means that they can trigger callbacks or functions when certain events occur within their application, such as user input or data changes. What’s great about Signals is that it allows for complete decoupling between logic and UI, making code more modular and easier to maintain.
Signals is actually a library created by the Pre-React team, who were looking for an alternative to React’s hooks mechanism as a state management solution. As a result, they’ve brought back event-driven programming to JavaScript, reducing dependency on the framework’s change detection/lifecycle mechanism. But here’s the best part: Signals are framework agnostic! Although there are implementations for popular frameworks, the core library is as vanilla as can be.
const count = signal(0);
when you invoke a signal, you get back the context of the invocation (i.e., this). this context has some unique properties that are important to know (note: underscore before variable means internal API).
When we access the count signal, it will add a subscription to change, and when we set a value for the count, it will add it as another listener to changes.
import { signal, computed } from "@preact/signals";
const count = signal(0);
const double = computed(() => {
return count.value * 2 <-- when count._value changes it will re-run this calculation
});
function Counter() {
return (
<button onClick={() => count.value++}> <-- when setting the count it will notify in all places on the change
{count} <-- when accessed (count == count.get) will add it as another listener to changes
</button>
);
}
One of the key differences between Signals and lifecycle-oriented frameworks is that the latter use a passive lifecycle, which means they wait for something to change before re-rendering relevant components. Angular, for example, has introduced a hybrid pattern in which you need to tell it to re-render the UI now. Signals, on the other hand, uses a pro-active, event-driven update system, meaning that they trigger an update when the value changes. This represents a promising shift towards a unified solution/ideology for re-rendering that focuses on updating only the elements related to the signals changed.
It’s worth noting that while Signals is a cool and promising solution, it’s still very young and untested. The Pre-React team has done a great job dealing preemptively with scaling issues, such as what happens when there are a lot of signals in one place or if there is a circular dependency issue that creates a ping-pong of updates signals. But it’s important to keep this in mind and continue testing and refining this event-driven pattern as a potential future replacement for passive lifecycle events.