Can Signals Replace React in State Management?
The term “Signals” is spreading like wildfire. When it comes to storing reactive state, most modern frameworks today, such as SolidJS, Qwik, Preact, and others, prefer signals.
It’s all because of the performance advantages and ease of use it provides. Think of signals as React’s useState
on steroids. It offers truly-reactive state management with fine-grained control over reactivity. And that’s not all; it also includes a ton of other features designed to boost performance and the developer experience.
In this article, we’ll look at Signals, what it is, and how it works, and compare them to React states. Also, we’ll go over some of the advantages and disadvantages of Signals over React. We’ll be talking mostly about SolidJS, but the same concept applies to frameworks like Qwik, Preact, and others.
What Are Signals?
Signals are not a new concept. It was inspired by an older framework called Knockout.js.
The idea behind knockout.js is simple: you have three basic components, i.e., View, ViewModels, and Observables. ViewModels are data components for the UI. A View is the UI element bound to the ViewModel. An Observable is a type of JavaScript object that notifies its subscribers when it changes. A ViewModel is a collection of observables, and a View can subscribe to one, allowing knockout to update it whenever the observables change.
var petViewModel = {
petName: ko.observable('Tommy'),
petType: ko.observable('Dog'),
};
ko.applyBindings(petViewModel);
Code language: JavaScript (javascript)
Signals work on a similar principle. When you create a signal, you’ll get a getter and a setter. A getter is a function that returns a value (in this case, the stored state), whereas a setter is used to set (or store) a value. It is similar to React’s useState()
, but instead of a getter, useState()
returns a value directly.
Now, because it’s a getter, we have to call it like a function to get the value anywhere in the code:
// In JSX
<div>
<h2>Counter: {counter()}</h2>
</div>
// Elsewhere
console.log(counter())
Code language: JavaScript (javascript)
But why does it use getters? Let’s find out.
Getter v/s Value
Signals return a getter rather than a value. Getters keep a reference to the actual value, so whenever we use one, we always get latest the value.
Take, for example, the following two code snippets, one in React and the other in SolidJS:
As you might expect, the setInterval
function in React will always contain the older value of the variable counter, whereas, in SolidJS, it will always contain the latest value.
This is only one advantage. Another advantage of using getters is the ability to create subscriptions and bring in reactivity. It’s the heart of signals, so let’s go over it in more detail.
How Do Signals Achieve Reactivity?
Signals are truly reactive, and the reactivity is achieved through the creation of subscriptions to the state. Let’s see how it’s done.
A signal must know what part needs to be changed in order to react to any change. Signals use getters to keep track of who is interested in state updates.
Getters keep track of all the contexts in which they are called. When the state of the signal changes, it knows which pieces need to be updated.
As a result, it only updates those pieces when the state changes. Take the below code snippet as an example:
export function Counter() {
const [counter, setCounter] = createSignal(0);
return (
<main>
<section>
<article>
<p>Counter: {counter()}</p>
<button onClick={() => setCounter(counter() + 1)}>Increment</button>
</article>
</section>
</main>
);
}
Code language: JavaScript (javascript)
The counter()
function is used deep within the main
element. Because it is used as a text node within a <p>
element, the context, i.e., the text node of the <p>
element, is subscribed to it when the code runs.
When the Increment
button is clicked, SolidJS only updates a particular text node inside the <p>
element, rather than the entire tree.
There is a high level of granularity when it comes to reactive updates. Not only does it prevent the component from being re-rendered completely, but it also updates just the text node inside of the element. That’s the reason why signals are so fast.
It also explains why a component in SolidJS runs only once, as opposed to React, where the components run whenever the state changes.
Signals v/s useState
We’ve already seen how signals can achieve fine-grained reactivity by using subscription patterns. But how does it stack up against useState
?
The useState
hook, as we know, returns the state variable as well as a setter function. Because the state value is directly returned, React has no way of tracking where it is used within the JSX or other components; thus, the only way to respond to a state change is to re-render the entire component tree.
Due to this, React must perform more computation than frameworks like SolidJS. Let us compare them side by side to see who performs better.
Component Level Performance
Consider the following scenario. Below are two embedded Codedamn Playgrounds, one with React and useState
and one with SolidJS and signals. Both playgrounds include a counter; let’s see how many times the component runs (or re-renders) in each of them.
(Make sure you open the browser console by clicking the “Toggle DevTools” button in the bottom right corner of the browser preview)
ReactJS Counter:
SolidJS Counter:
As you can see, a React component runs whenever the state changes, whereas a SolidJS component runs only once. The subsequent updates in SolidJS are applied directly to the locations where they are required.
Obviously, it’s a clear win for signals in terms of performance.
Prop Drilling
Since signals use subscriptions to respond to changes in the state, we can pass them deep down to other components while still updating only the necessary parts and not rendering all of the child components.
In React, if we pass a state deep down the component hierarchy, any change to the state will re-render all of the child components.
You can try it out for yourself by using the two Codedamn Playground embeds provided below. There are two counters in React, one in SolidJS. The counter
state is passed deep down at the bottom of the component hierarchy. Try increasing the counter to see how React and SolidJS perform.
ReactJS Counter:
SolidJS Counter:
As you can see, SolidJS renders each component only once initially. Further updates are applied straight to the final child component’s concerned text node.
However, with React, when the state changes, it will re-render all child components, even if the state variable is not used inside some of them. This is a problem for large-scale application performance. However, these types of re-renders can be reduced by using the memo
function, but prop drilling still triggers a lot of re-renders.
Do Signals and useRef works the same?
Although useState
requires a lot of re-rendering, you could argue that useRef
works similarly to useState
in that it maintains state across re-renders. So, is it an alternative to signals?
The answer is NO. Although the useRef
hook keeps the state, it does not cause any re-renders when its value changes. As a result, it is not reactive.
However, if you notice, signals can be thought of as a combination of the useState
and useRef
hooks, along with memoization. It is reactive like states, does not require any re-rendering like refs, and applies updates only where needed like memoization.
Advantages of Signals
Signals have numerous advantages over states. They are more performant and convenient to use. Let us look at some of the benefits of signals.
Performance
As previously demonstrated, signals outperform states. They operate on subscriptions, so only the concerned parts are updated. This is known as fine-grained reactivity. It means only updating the smallest (or fine-grained) element where a change is required.
Decoupled
Although we created and used signals within a component, it is not required. Signals can be created anywhere, even in a separate file. They can be exported from a file and used anywhere within an app.
This is a useful feature for separating the state from the component, but it may pose a problem because you must keep track of all the signals, which may reside elsewhere.
Synchronous
The updates to a signal state are synchronous, which means that the value is immediately available after the updates. React, on the other hand, processes state updates in batches asynchronously.
No Need For A Virtual DOM
Signals operate on a subscription pattern and apply changes to the required location directly within the real DOM. A Virtual DOM (VDOM) and any kind of diffing (or reconciliation) algorithms are unnecessary.
As a result, there is less overhead and better performance. This also reduces the amount of JavaScript that is shipped to the browser.
Therefore, you can see, most new modern frameworks, such as SolidJS and Qwik, do not use a VDOM at all and instead rely on signals to handle reactive states.
Disadvantages of Signals
Despite being far superior to states, signals have a few disadvantages. Let’s discuss a few of them one by one.
Initial Learning Curve
Signals may cause an initial learning curve due to the subscription pattern and use of getters. However, to solve this problem, most of the modern frameworks provide an easy-to-use and declarative API to improve the developer experience and reduce the learning curve.
Subscriptions Are Not Disposable
Subscriptions created to perform reactive updates are not disposable. For example, if we create a signal outside of a component, then, even after the component is unmounted, the signal continues to hold the memory.
However, frameworks such as SolidJS, Qwik, etc. do an excellent job of handling these cases and optimizing performance.
Should You Consider Switching To Signals?
We’ve seen that when it comes to performance, signals are by far the best. It offers fine-grained reactivity, no unnecessary re-renders, improved performance, less overhead due to the lack of a VDOM, ships less JavaScript to the browser, and so on.
React, on the other hand, provides more flexibility in terms of side effects, memoization, and so on. Also, as you may have heard, React is getting a new compiler that will intelligently memoize components and values, apply callbacks, and do a variety of other things to improve performance. In a nutshell, it will provide us with fine-grained reactivity while avoiding the pitfalls of signals.
Therefore, there is a tradeoff between the two, and both are well-suited to their respective use cases.
In my opinion, signals are currently far superior to states, and with new and even some older frameworks, such as Angular and Preact, adopting signals, it may become the go-to choice for state management in the future.
Conclusion
Signals are currently a hot topic, with many new and old frameworks adopting them. The reason for this is the fine-grained reactivity it provides through its subscription-based pattern.
It does not require a VDOM, so it has less overhead because there is no diffing going on during state updates, and it also ships less JavaScript to the browser.
It outperforms states because it does not perform unnecessary re-renders. In fact, there are no re-renders; it applies the changes directly to the required DOM element.
In a nutshell, signals are currently far superior to states, and it may become the go-to choice for state management in the future. It’s a good time to experiment with these new frameworks because they could be the future of web frameworks.
This article hopefully provided you with some new information. Share it with your friends if you enjoy it. Also, please provide your feedback in the comments section.
Thank you so much for reading 😄
Sharing is caring
Did you like what Varun Tiwari wrote? Thank them for their work by sharing it on social media.
No comments so far
Curious about this topic? Continue your journey with these coding courses: