Angular has recently announced a new reactive change detection system: Signals. To understand what advantages it brings over the current system, one must first understand the problem.

Change detection

Anyone who has ever developed web applications knows that a browser basically has only one thread. This leads to many asynchronous tasks that are processed one after the other in a queue. Because of this asynchronous nature, it is absolutely necessary to work with events and callbacks. So when an asynchronous task is finished, you inform everyone who is interested via an event. By the way, the browser takes care of this. It always knows when which event is ready and what to do next.

The problem, however, is that frameworks such as Angular or React also need to know when such an event occurs. Angular uses a Javascript framework called Zone.js for this.

Zone.js patches all possible APIs in the browser. For example Promise, setTimeout, setInterval, HTTPRequest and many more. It ‘opens’ these APIs, so that Angular can attach itself to them using the in-house solution: ng-zone, and thus reliably trigger the internal change detection whenever this is necessary.

Angular thus listens to everything that could lead to a possible change in the state of the application.

Example

Assume the following component tree:

                 ┌────────────────────────┐
                 │                        │
                 │                        │
                 │                        │
                 │        AppRef          │
                 │                        │
                 │                        │
                 └───────────┬────────────┘
                             │
                             │
                             │
                             ▼
                 ┌────────────────────────┐
                 │                        │
                 │                        │
                 │                        │
                 │      AppComponent      │
                 │                        │
                 │                        │
                 └──────────┬─┬───────────┘
                            │ │
┌─────────────┐             │ │             ┌──────────────┐
│             │             │ │             │              │
│             │             │ │             │              │
│             │             │ │             │              │
│      C1     │◄────────────┘ └─────────────┤      C2      │
│             │                             │              │
│             │                             │              │
└─────────────┘                             └──────────────┘

So as you can see, there is the usual AppRef and AppComponent which is the default in every Angular project. Two custom components, C1 and C2, are attached to the AppComponent(root).

I added some logic to the app.component, c-1.component and c-2.component:



Basically, this is as simple an application can be. Within the app.component.ts there is a button. C1 and C2 have a bit of HTML attached to a condition. So it has to check on updates what somethingFunctional() returns.

If you press the button in the root component this will trigger an event-binging. The browser will handle this event and since Zone.js is patching the API it recognizes this and therefore ng-zone and AppRef know that something has happened and there is an update. So all components will check for changes:



If you are now wondering why the outputs in C1 and C2 are duplicated, I can explain that. It is because of the function triggerSomething(). This action leads again to a change detection in all components.

The Problem

I’m sure you’ve seen through the problem long ago. ChangeDetection is expensive. It requires resources, especially when nothing has actually happened to the component itself. Due to this constant triggering of updates, the web application, depending on its size, quickly becomes slow and sluggish.

Angular already offers possibilities to optimise this process. These include change detection settings in the component itself, the use of observables and the use of pure pipes. In addition there are framworks who offer more reactive solutions like RxAngular.

Nevertheless, the developers want to move away from the old approach and have introduced a concept called Signals. From v16 onwards, it is already available to everyone for testing purposes. By disabling Zone.js, which is already possible today, and introducing Signals, you can drastically improve the performance of your web application.

Signals

Let’s pretend we’re a gym and we want to know how much money our members are bringing us. So we start with a service that manages the data. To do this, we generate ourselves a signal for the number of members. Initially we start without members:

199
members = signal<number>(0)

That’s really it, that’s what a signal looks like. You define it with the type it holds and an initial value.

Assuming that every member pays 19$ a month, calculating the incoming is quite easy by multiplying the amount of members with the costs. To do this everytime the amount of members changes a computed signal is what we need. It will run everytime the members signal changes and recalculate the income:

199
income = computed(() => this.members() * 19)

The computed function in turn also returns an object of the type signal with the corresponding computed value. Last thing we need is a function that increments the number of members by one:

199
200
201
addMember(): void {
this.members.update(n => n + 1)
}

the update method is a helper method which provides the current value of the mambers signal as parameter. The most important function of a signals is its set() method. There are two other helper methods: update() and mutate() which are all using set() under the hood.

That’s basically all we need in the service, the next thing to create is a component which lets a user maintain the members and presents the income. The service we just created:

199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import {computed, Injectable, signal} from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MembershipService {

  members = signal<number>(0)

  income = computed(() => this.members() * 19)
  addMember(): void {
    this.members.update(n => n + 1)
    console.log(this.income())
  }
}

The new component just injects the service:

199
200
201
202
203
204
205
206
207
208
209
import { Component } from '@angular/core';
import {MembershipService} from "../../services/membership.service";

@Component({
  selector: 'app-member-administration',
  templateUrl: './member-administration.component.html',
  styleUrls: ['./member-administration.component.scss']
})
export class MemberAdministrationComponent {
constructor(public membershipService: MembershipService) { }
}

the magic happens in the html part of the component:

199
200
201
<button (click)="membershipService.addMember()">Add Member</button>
<h2>{{membershipService.members()}}</h2>
<h3>Income: {{membershipService.income()}}$</h3>





That’s it. Due to the architecture of the signals, only the absolutely necessary parts of the application are updated. By using this pattern, the change detection is not triggered anywhere else in the tree. What is shown here is just a small sample of what Signals have to offer. Normally one distinguishes between readonly and writable signals to make the application more robust. There are also effects coming with the signal update that can be very useful. If you want to know more have a look at the documentation (https://angular.io/guide/signals).

I hope this post helps you, just try it yourself ;)



Since english isn’t my main language: Translated with www.DeepL.com/Translator (free version) ;)