BACK TO THE SERIESGuida galattica ad Angular · 銀河のガイド

Angular in 10 steps - 5 of 10

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.

TYPESCRIPT · コード
import { signal } from '@angular/core';

const counter = signal(0); // un signal che parte da zero

Tre operazioni da conoscere:

TYPESCRIPT · コード
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.

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, senza fare nient'altro

I 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é signalcomputed 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:

TYPESCRIPT · コード
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 resettata

La 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.

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(`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:

HTML · コード
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:

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>

Nei progetti esistenti troverai ancora *ngIf e *ngFor ovunque: 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:

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: '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}
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>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 set e update
  • 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 track obbligatorio 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.