Back to the homepage
Angular

Angular Signals: A New Feature in Angular 16

The implementation of signals into version 16 of Angular is another groundbreaking change that has been introduced recently. Angular signals affect many key aspects, such as data flow, change detection mechanism, component lifecycle, and use of reactive values. It is worth taking a closer look at the upcoming changes. They will certainly have an impact on the application development process.

What are Angular Signals?

Signals are Reactive Primitives, based on a pattern called Observer Design Pattern. According to this pattern, we have a Publisher that stores some value along with a list of Subscribers who are interested in it, and when the value changes, they receive a notification.

To put it simply, a signal “wraps” a certain value, giving you the ability to react to its changes. This mechanism is well-known in Angular because of RxJS, which provides us with several types of Subjects. The best analogy is BehaviorSubject, which, like a signal, has an initial value.

The goal behind the new version was to improve the change detection mechanism. Why, with so many options, such as improving Zone.js, introducing a “setState” style API, or simply using RxJS, did they go with signals?

Here are a few reasons behind Angular signals implementation:

  • Angular can keep track of which signals are read in the view. This lets you know which components need to be refreshed due to a change in state.
  • The ability to synchronously read a value that is always available.
  • Reading a value does not trigger side effects.
  • No glitches that would cause inconsistencies in reading the state.
  • Automatic and dynamic dependency tracking, and thus no explicit subscriptions. That frees us from having to manage them to avoid memory leaks.
  • The possibility to use signals outside of components, which works well with the Dependency Injection.
  • The ability to write code in a declarative way.

How do you use Angular Signals?

So far, we’ve learned the reasons why signals were introduced and the mechanism of how they work. Now it’s time to take a closer look at the API that enables us to use them.

In Angular, a signal is represented by interface with

  • the getter function, whose call returns the current value, and
  • the SIGNAL symbol that allows the framework to recognize it.

The reading of the value is recorded and is used to build a dependency graph between signals.

As a rule, signals are read-only – we want to get the current value and track its changes. However, Angular provides us with an interface that allows us to change the value using built-in methods:

The set method is used to change the currently stored value. Using the analogy of BehaviorSubject, this is the same as calling it the next method.

The update method is used to store a new value created from the current one. The mutate method, on the other hand, directly modifies the current value. It is useful when we want to modify an array or object without changing the reference (e.g. add an element to an array using Array.prototype.push).

Calling asReadonly returns a new signal that stores the same value but does not allow it to be modified. Going back to the Subject analogy, this would be using the asObservable method.

To create an instance of WritableSignal, we have the following function:

The equal function allows the user to define when two values (most useful from a practical point of view for two objects) are equal. The default function uses the comparison operator “===” except that objects and arrays are never equal, allowing you to store the values of these types and notify you when they change.

An example of using the methods of the WritableSignal interface is as follows:

One of the most commonly used RxJS operators is map. It allows you to create a new value that depends on the source. In the case of signals, the computed function is used:

The signal created in this way depends on the ones whose value we read inside the computation function. That means that its value will change only if at least one of the dependencies changes its value. The computation function cannot cause side effects, so we can only perform read operations inside it.

Checking whether the new value is the same as the previous one works on the same principle as the signal function. Similarly, we can also pass an equal function that overrides the standard comparison method. This allows us to improve performance by reducing the number of computations.

The signal created with the computed function dynamically tracks the values of the signals whose value was read during its last computation.

The greeting signal will always track the value of showName. However, if that value is false, it will not track changes in the name signal. In this case, a change in the name value will not change the greeting value.

In many cases, it is useful to trigger a side effect. This is a call to code that changes state outside its local context, such as sending an http request or synchronizing two independent data models. Referring to RxJS we have the tap operator, and in the case of signals, the effect function is used for this:

The effectFn, registered in this way, reads signal values and is created whenever any of them changes its value. A cleanup function, which can optionally be registered as its argument, is called before the next execution of effectFn. It has the ability to cancel the action that started with the previous call.

The timing of the effect is not strictly defined and depends on the strategy adopted by Angular. However, we can be sure of some principled rules when working with them. These are as follows:

  • The effect will be triggered at least once.
  • The effect will be triggered after at least one of the signals on which it depends (reads its value) changes.
  • The effect will be called a minimum number of times. That means that if several signals on which the effect depends change their value at one time, the code will be executed only once.

Since the effect responds to a change in the signal on which it depends, it always remains active and ready to respond to changes. By default, its cancellation is done automatically. If the manualCleanup option is set, the effect will remain active after the component or directive is destroyed. To cancel it manually, we can use the EffectRef instance.

Changing a signal value inside the effect is recommended. It can lead to unexpected errors, so it is treated as an error by default. To change this, set the allowSignalWrites in options.

Angular Signal-based components

Note: Signal components have been presented and described in the Signal RFC but are not yet available in the current version. The API and operation may still change.

At the outset, it should be noted that the functionalities described in this section work in both components and directives. For the purposes of simplicity, I will not list them in the article.

In order to use signals and their related functionalities in components and take advantage of the new change detection mechanism (more on that in a moment), you need to set the signals option in the @Component decorator:

A value of false means that we are using the current approach and Zone.js-based change detection. However, this does not preclude the use of both types of components and their coexistence within a single application.

To use the value stored by the signal in the template, call the getter function returned by the signal function:

Previously, we had to avoid function calls in the template. This was because the returned value was recalculated after each change detection, which could lead to performance issues. In this case, this is no longer an issue. The view will be refreshed when a change in the signal value is detected.

The current @Input decorator has been replaced by an input function that returns a Signal (read-only) storing the latest bound value. It takes a default value and an option object as arguments. If we create an effect in a component that uses an input value, it will not be executed until that value is available.

A new type of input available in this type of component is a model that returns a WritableSignal. That means it provides the ability to change its value, which propagates back to the parent, changing the value of the signal whose reference was passed to it, creating a kind of two-way binding.

Since we have a new input, the output has also changed. Introducing signals doesn’t change the way the output works. However, to keep the API consistent in place of the @Output decorator we get an output function that returns EventEmmiter:

Additionally, the decorators @ViewChild, @ViewChildren, @ContentChild, @ContentChildren that are used for querying element(s) from the template convert to the corresponding functions that return Signal:

Due to the new change detection mechanism, changes also affect the area of lifecycle hooks. These are no longer methods of the component class that the interface requires us to implement. Instead, they are functions that accept callbacks executed at the appropriate time. Activating them involves calling the appropriate function inside the constructor or another method of the component.

Three new hooks have also been introduced to execute code after a view rendering operation:

The function is executed after the next change detection cycle is completed. This is useful whenever you want to read or write from the DOM manually.

The function is executed after every DOM update during rendering.

This is a special kind of effect that, when triggered (by a change in the signal from which it reads the value), executes at the same time as afterRender.

From the set of previous hooks, the nature of two is left:

  • ngOnInit is replaced by afterInit, and
  • ngOnDestroy is replaced by beforeDestroy.

The timing of their execution is the same as their predecessors. That is, after the component is created and all inputs are set, and before the component is destroyed.

The other hooks make no sense in the new change detection system and their operation can be replaced by using signals:

  • ngOnChanges – used to respond to input changes. Since now the input itself is a signal you can use computed to create a new signal based on it or register the corresponding operations inside the effect.
  • ngDoCheck – reacts to each change detection cycle. Its operation can be transferred to effect.
  • ngAfterViewInit – allows you to perform actions after the template is rendered. This place is now taken by afterNextRender.
  • ngAfterContentInit, ngAfterViewChecked, ngAfterContentChecked – used to observe the results of queries from the view. Since queries are also signal-based and therefore reactive by default, signals can be used directly.

New Change Detection Mechanism

Let’s start with a reminder of how the Change Detection system has worked so far.

Angular tracks browser events (such as DOM events, HTTP request sending, or timers) using the Zone.js library. It extends and adds callbacks at runtime (aka monkey-patching) to objects (window, document) or prototypes (HtmlButtonElement, Promise) that can lead to changes in the data model.

When such an event occurs the framework does not know what specifically, or if anything at all, has changed. It fetches the new data and updates the view by comparing it with the existing data. It goes through the entire component tree, although most often only a small part of the application needs to be refreshed.

The number of checked components can be reduced by using the OnPush strategy. Components that meet at least one of the conditions, such as the occurrence of a DOM event, change of input, or an explicit mark of a component as requiring checking, and all its descendants are checked. In short, it gives information on when to check for changes, but not where to check.

Such a solution provides some advantages, especially in smaller applications. These include:

  • Directly storing using JS data structures.
  • Storing state anywhere.
  • Changing state easily, without the need for an additional API.

In practice, however, it has a number of disadvantages, noticeable especially for larger systems. These are:

  • Zone.js initialization consumes time and resources, the need for which further increases as the application grows larger.
  • The need to replace async/await with Promise, since keywords cannot be monkey-patched.
  • The standard browser API is modified which can lead to errors that are difficult to diagnose.
  • Disruption of the standard unidirectional data flow can lead to the well-known ExpressionChangedAfterItHasBeenCheckedError.
  • The use of external libraries or scripts that use the browser API can lead to a large number of unnecessary change detection cycles.
  • Acts as the source of application performance issues.

The use of signals for Change Detection provides greater control and granularity, thereby providing greater efficiency and a better developer experience. Signal components are not subject to global Change Detection. Instead, they are refreshed individually according to a fundamental principle:

Change detection will occur only when the signal, whose value we read in the template, notifies Angular the value has been changed. 

The granularity of the new mechanism is that it independently checks each view, or brick, from which the template is built – a static set of HTML elements, directives, or components – creating a UI and giving the ability to conditionally or repeatedly display parts of it.

The following template consists of one view:

Refreshing the UI at the view level is the most efficient solution. These are relatively small elements with not too many bindings, making the cost of such an operation small. Larger fragmentation would consume additional memory and time to track multiple dependencies. In contrast, larger and dynamic structures naturally split into views that can be updated independently.

An additional optimization is provided by the fact that inputs are now signals. The update of the input value occurs before the change detection cycle, not during, and the detection itself does not occur if the input is not read in the template. Changing the value bound to input does not refresh the parent view, either.

Integration with RxJS

Observables, available thanks to RxJS, are now widely used in both Angular itself and the entire ecosystem. Signals will take over some of these uses. However, because there are two different concepts behind these constructs, they can work brilliantly together.

Signals are synchronous, making them excellent for managing state, as well as representing values that change over time. Observables, on the other hand, are inherently asynchronous and represent a stream of data. RxJS also provides many tools that allow great management of complex, asynchronous operations.

The toSignal function is used to turn Observable into a signal:

This function subscribes to the Observable passed as an argument and changes the value of the returned signal every time a new value appears. The subscription occurs immediately so that the code that creates the Observable is not called unnecessarily. When the context in which the function was used, such as a component, is destroyed, automatic unsubscription occurs.

As long as Observable does not emit any value, by default the signal will store undefined. This is not always the best choice, so the option object contains an initialValue field whose value will initialize the signal.

Some Observables emit values synchronously (such as BehaviorSubject). In this case, we can set the requireSync option, which gets rid of the initial value handling. However, if this option is set and the passed Observable is created asynchronously, the function will show an error.

Observable offers the subscriber three types of notifications: next, error, and complete. Since the signal is only interested in the emitted values, the toSignal function does not handle errors and will show an error the next time you try to read a value from the signal. To prevent this, you must manually handle the error using a try/catch block or catchError operator.

The toObservable function is responsible for the conversion in the other direction:

When the created Observable is subscribed, it will create an effect inside which it will transmit successive signal values to subscribers. All new values are emitted asynchronously. That means that changing the signal value several times and synchronously will cause only the last one to be emitted, like this:

Summary

Angular signals bring a lot of changes, but also a lot of new possibilities. The introduction of signals improves optimization and gives space to implement further changes that can improve the developer experience, provides a great complement to RxJS, defines a new way to manage state, and (in the future) create components. I hope this article has provided a valuable introduction to the topic and a basis for further exploration. I encourage you to share your opinions on this new feature.

About the author

Milosz Rutkowski

Angular Developer at House of Angular. Web application development enthusiast with a zeal to learn more and more about Angular. Fan of clean code, good practices and well composed architecture.

Don’t miss anything! Subscribe to our newsletter. Stay up-to-date with the latest trends, tips, meetups, courses and be a part of a thriving community. The job market appreciates community members.

Leave a Reply

Your email address will not be published. Required fields are marked *