Angular Signals: what problems do they solve?

Alexei Shatz
Neudesic Innovation
6 min readMay 26, 2023

Background

If you are interested in Angular either as a developer, architect, project manager, or tech blog enthusiast, the topic of Signals coming to Angular should not be missed. The introduction of Signals presents the potential for a dramatic performance increase in Angular, arguably the most impactful change since its shift to TypeScript a decade ago. If you are on the fence about which JavaScript framework to use for your upcoming project, the introduction of Signals may just sway you towards choosing Angular.

While the primary focus of this article is on the problems solved by Signals, I will also cover an example using Signals in code.

Signals were released as “Developer Preview Only” in Angular 16 (current), but they are scheduled to be stable in the forthcoming release of Angular 17.

Prerequisites

Before we delve into the specifics of Signals, it’s crucial to understand automatic change detection in Angular and a bit about Angular components and the component hierarchy. If you’re already familiar with these topics, you might want to skip the following section.

Automatic Change Detection

Automatic change detection’s goal is to maintain synchrony between the model (your TypeScript code) and the template (your HTML). It achieves this by tracking data changes in the component tree from top to bottom. It starts by checking the parent, then the first child, then the second child, and so forth.

The below diagram is a representation of the component lifecycle in Angular.

  1. Update the bindings (such as the ngFor directive and the template)
  2. Run the ngOnInit lifecycle hook
  3. Update the DOM
  4. Run change detection for the child component
  5. Run the ngAfterViewInit lifecycle hook (Note — it’s important that ngAfterViewInit runs after change detection? More on this later).

Automatic change detection might initially seem flawless with its ‘automatic’ functionality. However, as you might have anticipated, it comes with a set of notable drawbacks.

In its most recent ng-conf talk on “A Design Review 10 Years Later”, one of the main design flaws highlighted was Angular team’s decision not to provide a ‘manual’ option for change detection (enter Signals). Although many aspects have been well-managed since the shift to TypeScript, enforcing an ‘automatic only’ change detection policy wasn’t among them.

Signals (vs. Auto Change Detection)

Automatic change detection works well for certain types of events but what about for simple value changes, like a UI button click intended to increment a value from 1 to 2? Does the entire component tree need to know about this simple value change? Obviously not.

In reality, only a small portion of the entire application state changes, and therefore, only a fraction of the components need to be re-rendered.

Several workarounds exist for running change detection only on a subset of the component tree, but none of them offer an elegant or complete solution.

Signals, however, aim to address this issue by introducing a publish and subscribe pattern in Angular.

With Signals, a component can subscribe to value changes, enabling it to be updated directly, without traversing the entire component tree (which can be a costly proposition in a large application).

Thus, as a developer, you can now guide Angular to discern which data changes are significant enough to trigger a check on the component tree.

Will Signals break my existing code?

Signals are fully backward compatible. Your team can upgrade your application(s) to Angular 17, and your libraries/code will continue working as is, without any modification. Should you choose to opt into Signals, you can do so at your own pace.

Aside from performance issues, what other problems does Signals aim to solve?

Expression Changed After It Has Been Checked

If you have spent some time with Angular, chances are you’ve encountered an ExpressionChangedAfterItHasBeenCheckedErrorat least once.

This error occurs when a value in the template gets updated after change detection has finished.

Your solution to this issue can vary greatly depending on the specific reason, as there are numerous ways this can occur. For instance, this can happen when a parent component is subscribed to an event in a child component, which then uses that event to update a value in the parent component. Angular throws NG0100 because the parent component has “already been checked”.

One potential solution could involve moving the variable you’re updating in the parent into the child, but depending on other dependencies on your parent component, this may not be viable.

Alternatively, you might consider moving the state to a shared service where it could be injected into necessary components.

With Signals, however, there’s another (often superior) solution to this issue. You can change (or add) the variable that gets updated from the child to the parent component to a signal. Since Signals do not trigger change detection, it won’t matter if the parent component has already been ‘checked’ when the value updates from the child component.

Signals in Action

Let’s consider a scenario where we want a value update in the view, based on a button press from the user, without triggering a component tree traversal.

We want to allow the user to multiply the value by two, divide by 10, square it, and clear the value (reset it to 1). In addition, we want to display an ‘Operations log’ indicating which operations have been performed on the initial value of 1.

Here’s a glimpse at the final product:

Taking a look at the code…

import { NgFor } from '@angular/common';
import { Component, signal } from '@angular/core';

@Component({
selector: 'app-mySignals',
templateUrl: './signals.component.html',
standalone: true,
imports: [NgFor],
})
export class MySignalsComponent {
operationsLog: string[] = [];
total = signal(1);

multiplyByTwo() {
this.total.update((val: number) => val *= 2);
this.operationsLog.push('multiply by two');
}

squared() {
this.total.update((val: number) => val * val);
this.operationsLog.push('square');
}

divideByTen() {
this.total.update((val: number) => val /= 10);
this.operationsLog.push('divide by 10');
}

clear() {
this.total.update((val: number) => 1);
this.operationsLog = [];
this.operationsLog.push('clear');
}
}

After creating my component, I initiated a writable signal by calling the signal function with the initial value of the signal and assigning it tototal .

To change the value of the writable signal you can either set it using the .set() method, or compute a new value based on the previous one using the .update() operation.

I opted for the .update() option in each of the operations methods:

<h1>Signals Example</h1>

<div id="value">
<p id="value-output">Total: {{ total() }}</p>
<p id="value-instructions">Select an operation to update Total via Signal</p><br>
<div id="value-btns">
<button (click)="clear()">Clear</button>
<button (click)="multiplyByTwo()">Double</button>
<button (click)="squared()">Square</button>
<button (click)="divideByTen()">Divide By Ten</button>
</div>
</div>

<h2>Operations Log</h2>
<ol id="log">
<li *ngFor="let operation of operationsLog">{{ operation }}</li>
</ol>

To access the signal in the template file above we simply reference it with total() as if it’s a function.

We now have a mechanism to updatetotal directly in four different ways, without triggering a single component tree traversal.

Conclusion

The introduction of Angular Signals adds another layer of complexity, leading to a steeper learning curve for developers. It undeniably requires more effort and consideration, adding another potentially necessary tool to a developer’s toolkit. As a result, Signals may increase the cost of training new hires on your application/stack. Despite these considerations, the trade-off is undeniably worthwhile.

Angular has always been renowned for its capability to deliver top-notch enterprise UIs. With the introduction of Signals, Angular opens the door to potentially substantial performance gains. By tackling this formidable challenge, the Angular team and community are eliminating an ‘automatic’ mechanism that previously constrained users. This courageous move deserves applause.

--

--

Alexei Shatz
Neudesic Innovation

Consultant at Neudesic, an IBM Company. Seeker of fewer mistakes.