Let’s take a closer look on what those shortcomings could be and how the Angular team wants to tackle those. In this post, we will dive deeper into the new approach the angular team is taking in terms of reactivity. In order to do this, we have to take a step back and look at pre 16 Angular.
Before releasing the latest major version of Angular, long-time rivals like React or Vue, but also newer frameworks like Qwik, Svelte and Solid had been slowly but surely leaving Angular behind in terms of features like server side rendering (SSR), reactivity features (ChangeDetectionStrategy) and developer experience – the latter being greatly improved with the introduction of Standalone Components in Version 15.
Introduction
In their latest version, Angular provides new possibilities to having fully reactive values. Reactivity describes the ability of a web framework to handle updates to the user interface. This usually happens when the user makes a certain input, thus changing the state or data actively or when this happens due to changes from an external source passively. This way the user will always have the most recently available data without having to manually refresh the website for example.
Angular’s change detection to ensure reactivity was relying on a library called Zone.js. Apart from a fairly big bundle size, in the default configuration, change detection is performed by checking the entire component tree and propagating changes. By using the ChangeDetectionStrategy.OnPush
, we can already partly avoid this suboptimal behaviour in regard to performance, but wouldn’t it be amazing to achieve this without shipping additional resources. With Signals, Angular has released a new means to detecting these changes and propagating them to the user interface – achieved without relying on the previously mentioned library Zone.js.
Writable Signals
Signals essentially are a new primitive type that can store values like so
const price: WritableSignal<number> = signal(25);
In contrast to an Observable, Signals always need an initial value.
This Signal will now hold a number representing the price of a certain item. It is possible to access its value by calling the variable as a function this.price()
or setting a new value directly via the provided setter function this.price.set(26)
.
An update
function lets us assign a new value based on the old one like so
this.price.update(price => price + 1)
Lastly, a mutate()
function can be used whenever we have a Signal containing an object that we don’t want to replace completely, but still make changes to some values within this object.
const ball = signal({name: 'ball', price: 42});
...
this.ball.mutate(value => {value.price = 43});
In this particular example, completely reassigning all values would not have made a big difference, but imagine just wanting to change one value in a big list of complex objects.
Computed Signals
We have the possibility of declaring a Signal, the value of which is derived from another one (or more). Let’s stay with the example of having variables representing prices. Assuming we want to have the sum of two items computed automatically when one of them changes, we can define our computed Signal as
const bellPrice: WritableSignal<number> = signal(21);
const helmetPrice: WritableSignal<number> = signal(45);
const sumOfPrices: Signal<number> = computed(() => bellPrice() + helmetPrice());
Angular now knows to update all occurrences of sumOfPrices
when the price of the bell or the helmet changes. These computed Signals are read-only for us developers which comes with the following advantages:
1. They are lazy-loaded. The computation of its value happens for the first time that the variable is actually read.
2. They are cached. Upon completion of the computation, the value is being stored and only computed again when a dependency changes.
One more fancy thing about computed Signals is that they can contain conditions. When taking our age example one step further where we want to display the sum of ages only if a user has the privilege in the application, we can evaluate this privilege in a Signal returning a boolean value and only evaluating the sum when said boolean value holds true
.
const showSumOfPrices = signal(false)
const sumOfPrices = computed(() => showSumOfPrices() ? bellPrice() + helmetPrice() : `Privilege to display sum of price is missing.`)
The neat part about this is that the prices of the bell or the helmet are never read as long as showSumOfPrices
is false
, thus never triggering any unnecessary updates to sumOfPrices
. As soon as showSumOfPrices
is however true
, the prices are being read and the sum of prices is evaluated for as long as it stays true
.
const showSumOfPrices = signal(false)
const sumOfPrices = computed(() => showSumOfPrices() ? bellPrice() + helmetPrice() : `Privilege to display sum of price is missing.`)
More news!
Lastly, one more great advantage, Signals are guaranteed to be glitch-free. Imagine a scenario with RxJS, where we have two variables that we change right after each other.
Let’s assume to have two products that have a price which we want to update. Upon setting the new prices right after each other, we will run into an intermediate state where one product will already yield the newly assigned value, whereas the other still holds the old one.
With Signals, this will not happen.
const bookPrice = signal(10)
const blenderPrice = signal(30)
this.bookPrice.set(11)
this.blenderPrice.set(33)
will update both prices at the same time. This is very important to highlight, because we do not want the change detection running with each update to a variable. I personally am really excited about this in particular!
Effect()
An additional operation which is useful in the context of Signals are effects. They can be created by calling effect()
and passing a lambda function as parameter. All changes to Signals passed into this lambda will cause the function in the effect to rerun. An effect will be executed at least once.
In general, effects should not be the go-to solution for every problem, but there are certain use-cases like execution behavior that template syntax does not support. They should not be used for propagating changes of the state of your application, e.g.
One of the possible outcomes are infinite circular updates or, less critical, unnecessary execution of change detection.
The most common way to set up an effect is by adding it to the constructor of your component, service or alike.
When there is a need for logging, they can come in handy or potentially when trying to quickly debug/verify certain behavior.
On destruction of the corresponding component, service or alike, an effect will be destroyed as well.
That’s all cool, but how and why does this even work?
First of all, congratulations on making it this far 😉
After reading through articles, watching videos about Signals and finally trying them out myself, I found myself curious as to how they even work. Let’s take a brief look!
When we initiate a new Signal price = signal(39)
, we will get a new Signal of type number. Apart from that, the Signal will be added to a list of observers. When updating the price by running price.set(40)
, this list of observers will be notified about the changes. That means, without any price()
occurrence, absolutely nothing will happen. Calling price()
will add the variable to the list of observers.
In short, calling set()
on a Signal, will notify all those occurrences where the getter on this particular Signal is being called. The same applies to effect()
. Whenever a value is updated, they will all be notified about the changes.
What does this mean for us as Angular developers?
Since as of v16, Signals are still in developer preview and you sure as hell do not want to rewrite (parts of) your application twice, it is highly encouraged not to refactor the entirety of your production application to Signals. You could however start with small leaf components that little to no other component relies on to get a first glimpse of this awesome new feature.
Apart from that, it is always highly encouraged to be somewhat up-to-date with your project’s dependencies, so in order to be prepared, you can definitely make your way to upgrading to a v16 Angular.
It will also still take a fairly long time until we can stop relying on Zone.js and having to ship it in our Angular applications – that is for applications that have been around for some time, new ones could potentially work without it entirely. But we need to start somewhere 🙂
So get yourself a new project with Angular 16 (if your current one can not be updated easily) and start experimenting yourself.
More features of Angular 16
This blogpost is heavily focusing on the reactivity feature, (I will say one last time – I Promise
– pun intended) namely Signals. There are however a lot more features and changes coming with v16 such as:
– SSR
– Hydration
– required Input
values to components and a corresponding transform
function along to it
to name a few.