In recent times, JavaScript and signals have gained attention as a powerful new tool for managing reactive states. But how did that come about?
Why did the JavaScript team need to implement signals when similar implementations already exist in various JavaScript frameworks?
In this blog post, we’ll dive into what signals are, explore how this "new" approach to development really works, and compare it to previous state management solutions.
What are signals?
First, let’s look at the definition of signals. They are defined with the following phrase:
“Signals are basic data units that can automatically alert functions or computations when the data they hold changes.”
So signals can work two ways, they can receive data and they can transmit data down the line. There are plenty of similar examples being used in different frameworks, which all have their little changes in implementation, but in fact are used to get the same thing in the end.
By introducing signals, the goal in JavaScript was standardizing signals and their usage and simplify state management in these scenarios.
This code demonstrates the usage of signals, computed values, and effects from the @preact/signals library.
First, a reactive signal is created with the name count and an initial value. The value of the signal can be accessed with count.value.
Computed value doubled presents a reactive value that is automatically calculated based on other signals. This is a read-only value which only changes when the signal values it depends on change. Implementation of computed signals can vary, so let’s see another example that showcases a slightly complex operations and the strength of computed values.
Looking at the output values, it can be seen that the value is computed initially and then changes to the fruits array. This is the basis of signals, that the computed values will output again only when the original values change.
Another example of computed signals is when multiple signals are composed to display some kind of value. The easiest example of this would be the following:
This example concatenates name and surname to produce a string that presents full name.
Last but not least, let’s talk about the effect() function. This function is used to run code in response to signal changes, as shown in the previous example with console.log commands. Effects are run in the same manner as computed signals, initially when loading the component and on every signal change. However, unlike computed signals, effect() does not return a signal. Instead, they are used to “catch” the changes that occur, like useEffect() in React.
Examples of similar signal usages in JavaScript
In the introduction, signals are defined as “new,” which is not technically true, as some variations of signal implementation have been used and discussed for some time now in various programming languages. While these approaches can achieve similar effects, they are often less straightforward and sometimes harder to read compared to signals.
React hooks comparison
Let’s look at the first example from the previous paragraph, this time using the useState and useEffect hooks.
At first glance, the amount of boilerplate code that needs to be written can already be seen. This example shows the use of three useEffecthooks: one that is initially run, one when the count changes, and one when doubled changes.
Hooks are not designed to offer the fine-grained reactivity that signals offer, so there is no automatic update of the doubled variable when a change happens to count. With signals, the changes are being propagated automatically without explicitly declaring dependencies.
Angular RxJS comparison
Long before they were introduced, developers had the option of using BehaviorSubject, which can be called an RxJS “in-house” version of signals, developed long before they became a JavaScript standard.
In many ways, BehaviorSubject is quite similar to signals. However, signals have the advantage of added simplicity and can be used without requiring an entire library. Here’s the count example from previous paragraphs, now written using BehaviorSubject:
By looking at this example, it can be seen that there is a significant difference between using BehaviorSubjects and Signals. BehaviorSubject creates an observable, which is not a value that can be simply extracted using .value.
This is why the process of transforming data to calculate a doubled value is more complex and requires more boilerplate than when using signals. Here, both the pipe and map operators from RxJS are needed to perform this simple action.
Additionally, because it’s an observable that provides a stream of data rather than a single value, the user must subscribe to it to start receiving values and unsubscribe when they are finished.
Simpler state management
The previous examples showcased how signals aim to improve boilerplate code and reactive programming on the component level, and the same benefits apply at the application level. Signals can also replace state management libraries that add significant overhead and complexity to a project.
When passing a signal, a new signal isn’t created; instead, a reference to the original signal is passed. This means that changes made in one part of the application immediately affect other parts, making the application more reactive.
Signals in the JavaScript ecosystem
The JavaScript Signals proposal is an initiative by TC39 to establish a standard for managing reactive states across JavaScript applications.
While JavaScript previously introduced a standard for promises, this proposal differs by focusing on a foundational reactive model that frameworks can adopt, facilitating interoperability across libraries like React, Angular, and Vue. By focusing on features like automatic dependency tracking, lazy evaluation, and memoization, Signals aim to simplify functional reactive programming and enable efficient, glitch-free state updates.
The current proposal encourages early experimentation, allowing developers to test the Signal API in frameworks before moving to further standardization stages in order to one day reach standardizing signals functional.
This is a simple example that can be found in the proposal that illustrates class-based signal usage. The code alternates between displaying odd and even values on the screen.
Conclusion
The purpose of standardizing signals in the JavaScript ecosystem is to address the need for fine-grained reactivity and simpler state management in web applications. This concept helps minimize boilerplate code and boosts developer productivity by providing a more declarative way to write and manage code, while also addressing performance concerns.
As they come with a minimal learning curve, we have started to implement them with every new development in our projects. The code has gotten more concise, readable, and understandable for us, as well as for new developers who come into our team.