← Back to the seriesThe Galactic Guide to Angular Β· 5 / 10

Signals: readable reactivity

Signals: readable reactivity

I have mentioned them twice; it is time to explain them properly: signals are how modern Angular handles state and reactivity. If you come from React or Vue, the idea will feel familiar; if you come from "old" Angular, prepare to write much less code for the same result.


In this chapter, we cover signals, computed, linked signals, effects, and the new template control flow (@if and @for), which goes hand in hand with signals.


πŸ”” What are signals?


A signal is a container for a value with a superpower: it tracks who uses it. When the value changes, every part of the app that reads it updates automatically. No events to handle, no subscriptions to remember to close.

TYPESCRIPT
import { signal } from '@angular/core';

const counter = signal(0); // a signal starting at zero

Three operations to know:

TYPESCRIPT
1// read: a signal is a function, call it
2console.log(counter()); // 0
3
4// replace the value
5counter.set(5);
6
7// update using the current value
8counter.update((valore) => valore + 1);

The function-like syntax, counter(), feels strange at first. But that is the trick: calling the function tells Angular exactly who reads what, and when the value changes, it knows precisely what to update. This makes signal reactivity readable from top to bottom: follow the calls and you know what depends on what.


βš™οΈ Computed: derived values


A computed is a signal whose value is calculated from other signals. You never update it manually: it recalculates itself when its dependencies change.

TYPESCRIPT
1import { computed, signal } from '@angular/core';
2
3const counter = signal(0);
4const doubleCounter = computed(() => counter() * 2);
5
6counter.set(3);
7console.log(doubleCounter()); // 6, without doing anything else

Computed signals are also lazy and efficient: calculation happens only when someone reads the value, and the result is cached until dependencies change. Use them whenever data can be derived from other data: totals, filters, validity flags. If you catch yourself manually synchronizing two properties, almost always one should have been a computed signal.


πŸ”— Linked signals


There is a case neither signal nor computed handles well: a value the user can change, but that must reset when something upstream changes. A classic example: the selected size on a product page. The user can choose it (so it needs to be writable), but changing the product should restore the default selection (so it must derive from another source).


That is what linkedSignal is for:

TYPESCRIPT
1import { linkedSignal, signal } from '@angular/core';
2
3const taglie = signal(['S', 'M', 'L']);
4
5// writable, but recalculates when `taglie` changes
6const tagliaSelezionata = linkedSignal(() => taglie()[0]);
7
8tagliaSelezionata.set('L');   // the user selects L
9taglie.set(['XS', 'XL']);     // a new product arrives...
10console.log(tagliaSelezionata()); // 'XS': selection reset

The key difference from a computed signal: a computed is read-only; you can never call .set() on it. A linked signal is writable like an ordinary signal, but derives its value again when its source changes.

πŸŒ€ Effects: reacting to changes


An effect runs code whenever the signals it reads change. It is for side effects: logging, saving to localStorage, synchronizing with external libraries.

TYPESCRIPT
1import { Component, effect, signal } from '@angular/core';
2
3@Component({ /* ... */ })
4export class DemoComponent {
5  counter = signal(0);
6
7  constructor() {
8    effect(() => {
9      console.log(`the counter is ${this.counter()}`);
10    });
11  }
12}

Advice from experience: effects are where you should not put data derivation logic. If you are calling .set() on another signal inside an effect, stop: what you wanted was almost always a computed or linked signal.


πŸ–₯️ The new control flow: @if and @for


Since Angular 17, templates have a new syntax for conditions and loops: @if and @for blocks written directly in the markup, with no imports. They replace the old *ngIf and *ngFor and are more readable and faster.


@if, including @else:

HTML
1@if (isVisible()) {
2  <p>Questo testo appare solo se isVisible Γ¨ true</p>
3} @else {
4  <p>Altrimenti appare questo</p>
5}

@for, for iterating a list. Notice track: it is required and tells Angular how to identify each element, so list changes update only the necessary DOM. There is also @empty for an empty list:

HTML
1<ul>
2  @for (item of items(); track item) {
3    <li>{{ item }}</li>
4  } @empty {
5    <li>Niente da mostrare</li>
6  }
7</ul>

Existing projects still contain *ngIf and *ngFor everywhere: they work, and migration need not be rushed (there is even a dedicated schematic, ng generate @angular/core:control-flow). But use blocks for new code: fewer imports, fewer surprises.

Putting it all together


A small but complete component with a signal, a computed and control flow:

TYPESCRIPT
1import { Component, computed, signal } from '@angular/core';
2
3@Component({
4  selector: 'app-carrello',
5  templateUrl: './carrello.component.html'
6})
7export class CarrelloComponent {
8  articoli = signal([
9    { nome: 'Vinland Saga vol. 18', prezzo: 7.5 },
10    { nome: 'D&D dice', prezzo: 12 },
11  ]);
12
13  totale = computed(() =>
14    this.articoli().reduce((somma, a) => somma + a.prezzo, 0)
15  );
16
17  svuota() {
18    this.articoli.set([]);
19  }
20}
HTML
1@if (articoli().length > 0) {
2  <ul>
3    @for (articolo of articoli(); track articolo.nome) {
4      <li>{{ articolo.nome }} - {{ articolo.prezzo }} €</li>
5    }
6  </ul>
7  <p>Total: {{ totale() }} €</p>
8  <button (click)="svuota()">Empty cart</button>
9} @else {
10  <p>Il carrello Γ¨ vuoto.</p>
11}

No subscriptions, no manual update management: change the data and the interface follows. That is all, and it is a lot.


Where we are now


Today's pieces, recapped:


  • signal: reactive state, read by calling it and changed with set and update.
  • computed: a derived, read-only value that recalculates automatically.
  • linkedSignal: writable, but resets when its source changes.
  • effect: code reacting to changes, only for side effects.
  • @if / @for: modern template control flow, with required track in loops.

Signals complete the picture of reactivity. In the next chapter, we immediately put them to work with forms: collecting user data, validating it and handling submission using Angular's two approaches.

Fullstack developer in Milan. Writes about Angular, the JavaScript ecosystem and AI.