Angular in 10 steps - 5 of 10

05 - Signals: la reattività che si legge
Te li ho anticipati due volte, è ora di parlarne davvero: i signals sono il sistema con cui Angular moderno gestisce stato e reattività. Se arrivi da React o da Vue l'idea ti suonerà familiare; se arrivi da Angular "vecchio", preparati a scrivere molto meno codice per ottenere lo stesso risultato.
In questo capitolo vediamo cosa sono i signals, i computed, i
linked signals, gli effect e il nuovo control flow del template
(@if e @for), che con i signals va a braccetto.
🔔 Cosa sono i signals
Un signal è un contenitore per un valore, con un superpotere: tiene traccia di chi lo sta usando. Quando il valore cambia, tutte le parti dell'app che lo leggono si aggiornano da sole. Niente eventi da gestire, niente subscription da ricordarsi di chiudere.
import { signal } from '@angular/core';
const counter = signal(0); // un signal che parte da zeroTre operazioni da conoscere:
1// leggere: il signal È una funzione, si chiama
2console.log(counter()); // 0
3
4// sostituire il valore
5counter.set(5);
6
7// aggiornare a partire dal valore corrente
8counter.update((valore) => valore + 1);Quella sintassi da funzione, counter(), all'inizio fa uno strano effetto. Ma è
proprio il trucco: chiamando la funzione, Angular sa esattamente chi legge
cosa, e quando il valore cambia sa esattamente cosa aggiornare. È questo che
rende la reattività dei signals leggibile dall'alto in basso: segui le chiamate
e sai chi dipende da chi.
⚙️ Computed: valori derivati
Un computed è un signal il cui valore si calcola a partire da altri signals. Non lo aggiorni mai a mano: si ricalcola da solo quando cambiano le sue dipendenze.
1import { computed, signal } from '@angular/core';
2
3const counter = signal(0);
4const doubleCounter = computed(() => counter() * 2);
5
6counter.set(3);
7console.log(doubleCounter()); // 6, senza fare nient'altroI computed sono anche pigri ed efficienti: il calcolo avviene solo quando qualcuno legge il valore, e viene messo in cache finché le dipendenze non cambiano. Usali ogni volta che un dato è derivabile da altri: totali, filtri, flag di validità. Se ti accorgi di tenere sincronizzate a mano due proprietà, quasi sempre una delle due doveva essere un computed.
🔗 Linked signals
C'è un caso che né signal né computed coprono bene: un valore che l'utente
può modificare, ma che deve resettarsi quando cambia qualcosa a monte.
Esempio classico: la taglia selezionata in una pagina prodotto. L'utente può
sceglierla (quindi serve scrivibile), ma se cambia il prodotto la selezione va
riportata al default (quindi deve derivare da un'altra sorgente).
Per questo esiste linkedSignal:
1import { linkedSignal, signal } from '@angular/core';
2
3const taglie = signal(['S', 'M', 'L']);
4
5// scrivibile, ma si ricalcola quando `taglie` cambia
6const tagliaSelezionata = linkedSignal(() => taglie()[0]);
7
8tagliaSelezionata.set('L'); // l'utente sceglie la L
9taglie.set(['XS', 'XL']); // arriva un prodotto nuovo...
10console.log(tagliaSelezionata()); // 'XS': selezione resettataLa differenza chiave con un computed: il computed è di sola lettura, non puoi mai fare
.set()su di lui. Il linked signal è scrivibile come un signal normale, ma torna a derivare il suo valore quando la sorgente cambia.
🌀 Effect: reagire ai cambiamenti
Un effect esegue del codice ogni volta che i signals che legge cambiano. Serve per i side effect: log, salvataggi in localStorage, sincronizzazioni con librerie esterne.
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(`il contatore vale ${this.counter()}`);
10 });
11 }
12}Un consiglio da chi ci è passato: gli effect sono il posto dove non mettere
la logica di derivazione dei dati. Se dentro un effect stai facendo .set() su
un altro signal, fermati: quasi sempre quello che volevi era un computed o un
linked signal.
🖥️ Il nuovo control flow: @if e @for
Da Angular 17 il template ha una sintassi nuova per condizioni e cicli: blocchi
@if e @for scritti direttamente nel markup, senza importare niente. Prendono
il posto dei vecchi *ngIf e *ngFor e sono più leggibili e più veloci.
@if, con tanto di @else:
1@if (isVisible()) {
2 <p>Questo testo appare solo se isVisible è true</p>
3} @else {
4 <p>Altrimenti appare questo</p>
5}@for, per iterare una lista. Nota il track: è obbligatorio e dice ad
Angular come riconoscere ogni elemento, così ai cambi della lista aggiorna solo
il DOM necessario. C'è anche @empty, per il caso lista vuota:
1<ul>
2 @for (item of items(); track item) {
3 <li>{{ item }}</li>
4 } @empty {
5 <li>Niente da mostrare</li>
6 }
7</ul>Nei progetti esistenti troverai ancora
*ngIfe*ngForovunque: funzionano e non c'è fretta di migrare (esiste anche uno schematic apposta,ng generate @angular/core:control-flow). Ma per il codice nuovo usa i blocchi: meno import, meno sorprese.
Mettiamo tutto insieme
Un componente piccolo ma completo, con signal, computed e control flow:
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: 'Dadi per il D&D', 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}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>Totale: {{ totale() }} €</p>
8 <button (click)="svuota()">Svuota carrello</button>
9} @else {
10 <p>Il carrello è vuoto.</p>
11}Niente subscription, niente gestione manuale degli aggiornamenti: cambi i dati e l'interfaccia segue. Tutto qui, ed è tanto.
Dove siamo arrivati
Ricapitolando i pezzi di oggi:
- signal: stato reattivo, si legge chiamandolo, si cambia con
seteupdate - computed: valore derivato, di sola lettura, si ricalcola da solo
- linkedSignal: scrivibile, ma si resetta quando cambia la sorgente
- effect: codice che reagisce ai cambiamenti, solo per side effect
- @if / @for: il control flow moderno del template, con
trackobbligatorio nei cicli
Con i signals si chiude il quadro della reattività. Nel prossimo capitolo li mettiamo subito al lavoro sui form: raccogliere dati dagli utenti, validarli e gestire l'invio, nei due modi che Angular offre per farlo.