What are Angular Signals?
In plain terms, a Signal is just a value. However, besides the data it provides, it has some additional properties. Essentially, it serves like a wrapper that both contains the value and performs some additional functions. It has the ability to notify every consumer interested in that value when it changes. So this is a new reactive primitive that comes to Angular development – and is available out of the box in the framework.
A Signal can contain any kind of data structure. It can be as simple as containing a string and as complex as containing a graph or n x m matrix.
In the screenshot above, we see an example of how a signal can be defined. We have two signals, one of which contains a string and the other one contains a complex matrix marked with n x m. Both of these are defined in the same manner, like a function calling the signal function and giving it an input property.
I intentionally said “like a function” because in the template, it’s being called as a function userName(). That’s a getter function. Under the hood, this tells Angular to track where the signal is being used from. That way, change is being triggered only when change is being made. Additionally, the algorithm behind Signals also has a Memoization implementation; when the same value gets called two or more times, it will only execute the first one, then store that first result and return it for the next calls. This works towards improving the Angular Change detection cycles and reducing the number of changes that are required in order to update the DOM, thus boosting performance in large applications.
How Angular Change Detection Runs
I am not going to dive deep and explain how the whole Angular change detection system runs; you can find that better explained here. However, I am going to show an image of the result when a change happens in a default Angular application with multiple components and different node trees. This is a visualization of the way a small change can require all of the components in the DOM tree to check for updates and run a change detection cycle.
As you can imagine, there is a lot of redundancy and a lot of unnecessary checks that run under the hood. The reason all of that’s happening is because of zone.js and the way the current change detection runs. Basically, zone.js runs in most(about 98%) of Angular applications. And since Angular doesn’t have a way to tell which component is being changed, it uses zone.js to “monkey-patch” the event. It checks and compares all components to find where the change happened and which nodes need to be updates. But as you can imagine, the bigger the application, the more checks and updates that takes, thus slowing the process down bit by bit.
Which brings us to the next topic – which is, again, Signals!
Angular Signals will improve change detection
The new Angular reactive primitive Signal means that we will be able to dismiss that changeDetection strategy and completely remove zone.js from our applications. Signals are great at knowing where the change happened, and they’re aware of subscribers. As such, the functionality will run change detection only update affected components. In turn, that reduces performance costs by optimizing the process and reducing the number of checks needed. Basically, this now works the way it should’ve been working from the start.
Signals are completely independent from the component tree and the change happens only in the components that are subscribers to the signal that is being changed
They are solving the Diamond problem (move about it here) that occurs when we are using Behavior Subject and we try to combine multiple of them in order to calculate a value. See the screenshot for an example.
We can see that a simple change in the behaviour subjects would trigger a multitude of changes and recalculations. Each and every one of those would trigger Angular’s CD cycle.
What’s more, every time we click the Change Values button, the change would be recalculated even though we are setting it to the same values.
Let’s compare that to the way it would work with Angular Signals.
First of all, it takes a lot less code to do this. And no matter how many times we click the change values button, thanks to Memoization, we’re only calculating and updating the result once.
One other thing worth mentioning (and that I’ve mentioned several times already. I’m excited about it) is the fact that zone.js and automatic change detection are gone and out the window. We no longer run change detection in each and every component that we own in the app; now, we only do it in the components that are subscribed to the signal whose value is being changed.
Also note the function we have available for Signals is a set that is used for setting/updating the value of a signal which accepts a new value and notifies its dependents. Signals also support APIs like update and mutate, which accept updateFn (updates value of the signal based on its current value) and mutatorFn ( updated current value by mutating it in place).
Signals are fancy!
Applications that are already using OnPush , RxJs and async pipe are already fully set up for Signals. With slight code moderations, they can be refactored to use the new signals logic. The async pipe and all those subscriptions and unsubscriptions and takeUntil will be redundant. The necessary logic inside the ngOnDestroy function will become history. Change detection strategy such as OnPush will completely disappear – since we will no longer have any use for it – and the Signal will be able to take care of its own value and where it will be updated.
Now a few things about Signals to get more familiar with them:
Signals can be writable or read-only. There are three types of signals:
- Writable Signals (Basic ones, like from the count signal example above)
- Computed Signals – Signals whose value depends on, or is being derived from other signals. The derivedValue case above, which is being computed from multiple signals, is a good example of how a computed signal works.
- Effects is a slightly more complicated signal that needs to be defined in a constructor. Or it needs to provide an injector service in order to work properly. Here is a screenshot with an example of how to define each:
Effect Signal with injector provider as its second parameter
Effect Signal defined inside the constructor
Effects would be good mostly for logins data and analytics/debugging purposes. Keeping the data in sync with the window.localStorage , adding custom DOM behavior that can’t be expressed with template syntax , performing custom rendering to a <canvas> , or some charting library. They should most definitely be avoided for for propagation of state changes and we should always be careful when using them we should also take care of destroying them ( similar to how be unsubscribe when subscribing to a subject).
The new Angular Signals aren’t just good – they’re great. I expect them to add as a complementary tool to what we have in the existing Angular framework and the ecosystem revolving around it. They won’t be replacing RxJs but they will complement it and be used together in order to create a smooth developer experience and lightning-fast Angular applications. They can improve and boost speed and performance significantly. Also, state control and changes to it will change in a very positive way such that perfect balance would be easily attainable and sustainable. They are still in developer preview and some things will definitely change so be wary when trying to refactor all your Angular applications. We may still be a version or two too early to do it.