Angular Signals!! What’s the big deal?

Introduction

Angular introduced signals in their version 16 as developer preview. With version 17, signals are stable. In this article, let us delve into:

What signals areWhy we should use them

Front-end application development relies heavily on providing an enhanced user experience, which is closely tied to the runtime performance of the application. This article will explore how utilizing signals in our code can significantly improve the runtime performance of Angular applications. A basic understanding of Angular’s change detection mechanism will help in understanding these concepts.

Change Detection Mechanism in Angular

Whenever the application model (data) changes, Angular needs to update the view to maintain synchronization between data and the UI. Angular achieves this through its change detection mechanism.

Consider an asynchronous event occurs in our application, such as:

1. User interactions with the page (e.g., selecting an item from a list or clicking a button)

OR

2. Receiving data in response to an asynchronous request to the server

It will cause changes in the data model. For instance, when a user selects an item from a list, it triggers changes in the data model. Angular must then identify these changes and re-render only those components that use the modified data model in their templates. This is where Angular’s “change detection mechanism” comes into play.

Angular’s application view is composed of a tree of components. By default, Angular runs change detection on the entire component tree, from top to bottom, periodically. However, frequently running the change detection mechanism on the entire tree of components can potentially hinder the runtime performance of the application.

Performance Optimization

To optimize performance, we can:

1. Run the change detection only when necessary.

2. Skip parts of the application while running change detection.

This is where the OnPush strategy becomes crucial.

OnPush Strategy

By default, change detection runs on the entire component tree. However, we can configure the change detection strategy for a component as “OnPush”. When an event occurs outside the scope of the “OnPush” root component, the change detection mechanism will skip the subtree of the “OnPush” root component unless its input properties are changed.

Thus, performance optimization can be achieved by setting the change detection strategy to “OnPush” for a component.

However, it is essential to use the “OnPush” strategy cautiously to avoid potential issues. Let’s delve into these potential issues.

Let’s consider a simple counter example to illustrate the issue:

Component tree

Suppose we have one parent component and two child components. The parent component has a property ‘appCtr’ which is an object holding ‘counter’ as an integer value. It also has a button on its template. When the user clicks the button, the counter value is incremented through the function increment(). Our parent component code will be as under.

ParentComponent.ts

The two child components are supposed to display the current (incremented) value of the counter. To optimize performance, the change detection strategy for the child components is set to OnPush.

The Issue

When the user clicks the button, the counter (property of appCtr object: line no.9 above) increments, triggering the change detection mechanism. However, due to the OnPush strategy, the change detection mechanism skips the child components and their subtrees. Therefore, the incremented counter value is not reflected in the child components. Each child component acts as the root of its own subtree of OnPush components, making the ‘click’ event on the parent out of scope for the child components.

Example Scenario

Let’s say the user clicks the button several times, incrementing the counter to 5, as displayed in the figure above. However, the child components still show the counter value as 0. This discrepancy arises because the child components are not receiving notifications of the change in the counter value.

The Solution

The issue here highlights the need for a way to notify the child components about changes in the property value, especially when their change detection strategy is set to OnPush for runtime performance optimization.

What if the counter value itself could send notifications whenever it changes?

Signals Come to the Rescue

In typical scenarios, values like the counter mentioned earlier are often static. They can’t perform actions such as sending notifications. However, functions are powerful as they can execute actions. What if we provide a functional wrapper to these values? This way, values gain the capabilities of functions and can perform actions, such as sending notifications when they change. This concept is where “Signals” come into play.

A value with a functional wrapper around it becomes a Signal. Unlike a plain value, a Signal has the ability to send notifications about its changes.

Definition of Signal

A Signal is essentially a functional wrapper around a value. When the value changes, the Signal notifies all interested consumers about this change.

Let us first see Signals syntax and usage. Later we will see modifying the counter example to use signals.

Signals can be either writable or read-only.

Creating a signal: Writable signals

let counter = signal(0)

We create signal by calling the signal function and passing the initial value as above. This is how we create writable signals.

Reading a value from the signal:

The syntax for reading the value of signal:

counter()

Calling a signal, reads its value as shown above. A signal’s value is read through a getter function. Thus, the framework knows where the signal is used.

Updating the value of writable signal

const counter = signal (10) … Here we are setting initial signal value as 10.

It is possible to change the value of a writable signal in two ways:

a. using ‘set()’ method to change the value directly.

counter.set(13); // changes signal value to 13.

b. using ‘update()’ method to compute a new value from the previous one.

counter.update(value => value +1);

Computed signals

Computed signals get their value from other signals. We pass a function to computed(). The passed function tells how to derive the value of the computed signal.

const n = signal ( x);
const sqr = computed ( () => n() * n());

Here, sqr is a computed signal and depends on another signal, n. Whenever n is updated, angular knows that anything depending on n or sqr must be updated.

Counter Example with Signals

Let’s revisit our counter example. This time, we’ll modify Child2Component to receive the sCounter property which is a Signal, while Child1Component continues to receive counter as a plain value. To achieve this, we declare one more property in parent, appCtrSignal which is a signal that holds sCounter. This is the Input() property for Child2 component. On click of button now we increment counter and counterSignal both. Following is the ParentComponent.ts file with signals. (Refer to line no.9)

parent.component.ts (with signals)

Consider that the user clicks the button several times, incrementing the counter values in the parent component with each click.

Now new property appCtrSignal being a signal, the Child2Component receives notifications whenever it changes, ensuring it always displays the latest value.

On the other hand, Child1Component stays static. Child1Component continues to display the initial value (0 in this case) because it displays the appCtr.counter which is a plain value.

Following figure shows the output in the browser.

The complete example code can be found at the GitHub link.

GitHub – sangeeta-brewtechblue/angular-signals-demos: This repo contains a demo that compares signals with plain values and shows how runtime performance can be improved by using signals.

Note

When input property of OnPush component changes angular runs change detection. In this example, an object (appCounter)is considered as input property. In case a mutable object is received as input and the object reference is preserved, angular will not run the change detection even though the object’s property (counter) is changed. This is because previous and current value point to the same object reference.

In this article we have seen how signals can be used to improve runtime performance of our application. Signals also add reactivity to code and that could be a topic for another blog post.

Angular Signals!! What’s the big deal? was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Comment

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