Una nuova entusiasmante funzionalità è arrivata su Angular: i Segnali (Signal). Forniscono un nuovo modo di comunicare ai nostri template che i dati sono stati cambiati. Ciò migliora il rilevamento delle modifiche, aumenta le prestazioni e rende il nostro codice più reattivo.
Puoi trovare il codice di “Angular Signals Intro” su https://github.com/Impesud/angular-signals-intro e https://stackblitz.com/edit/angular-signals-intro
Si parlerà di alcuni concetti fondamentali per i segnali: change detection, computed signal, consumers, getter, signal components, consumer interest, RxJs, observables, service.
Perchè usare i Signal
Iniziamo con un semplice esempio senza l’uso dei segnali. Supponiamo che tu stia scrivendo codice per eseguire alcune operazioni matematiche di base.
1
2
3
4
|
let x = 5;
let y = 3;
let z = x + y;
console.log(z);
|
Cosa stampa questo codice sulla console? Lancia come risultato 8.
Poi, se cambiamo il valore di X, cosa stampa Z ora?
1
2
3
4
5
6
7
|
let x = 5;
let y = 3;
let z = x + y;
console.log(z);
x = 10;
console.log(z);
|
Lancia ancora 8! Questo perché Z ha già un valore assegnato da quando l’espressione viene valutata per la prima volta. La variabile Z non reagisce ai cambiamenti in X o Y.
Ma vogliamo che le nostre variabili reagiscano ai cambiamenti!
Uno dei motivi per cui utilizziamo Angular è creare siti Web reattivi, come nel nostro esempio: https://stackblitz.com/edit/angular-signals-intro
Quando l’utente aggiorna le quantità sul carrello, le variabili correlate (come subtotale e tasse) dovrebbero reagire e adeguare i costi. Se l’utente sceglie di eliminare un articolo dal carrello, vogliamo che le variabili correlate reagiscano e ricalcolino correttamente i costi.
Con i segnali, il nostro codice può diventare più reattivo. Il nostro esempio precedente implementato con questo nuovo strumento sarebbe simile a questo:
1
2
3
4
5
6
7
|
const x = signal(5);
const y = signal(3);
const z = computed(() => x() + y());
console.log(z()); // 8
x.set(10);
console.log(z()); // 13
|
Il codice precedente definisce due segnali: X e Y, e fornisce loro i valori iniziali di 5 e 3. Definiamo quindi un computed signal Z, che è la somma di X e Y. Poiché i segnali forniscono notifiche di modifica, quando i signal X o Y cambiano, tutti i valori calcolati da tali segnali verranno ricalcolati automaticamente. Questo codice è ora reattivo!
Un computed signal reagisce e ricalcola quando uno dei suoi signals dipendenti cambia. Se un segnale è associato a un template e cambia, il change detection di Angular aggiorna automaticamente qualsiasi view che legge il signal. E l’utente vede il valore modificato.
Quindi la risposta a “perché usarli?”:
- Forniscono più reattività.
- Ci offrono un controllo più preciso sul change detection, quindi migliora le prestazioni.
Come creare un Signal
1 |
quantity = signal<number>(1);
|
La sintassi precedente crea e inizializza un segnale utilizzando la funzione del costruttore.
Facoltativamente, si può fornire un parametro di tipo generico per definire il tipo di dati del segnale. Un segnale può essere una stringa, un numero, un array, un oggetto o qualsiasi tipo di dati. In molti casi, il tipo di dati può essere dedotto e il parametro di tipo generico non è necessario.
Si passa al costruttore il valore predefinito del segnale. Un segnale ha sempre un valore, a partire da quello predefinito.
Dal nostro esempio presente su https://stackblitz.com/edit/angular-signals-intro:
1
2
3
4
5
6
7
8
|
quantity = signal<number>(1);
qtyAvailable = signal([1, 2, 3, 4, 5, 6]);
selectedShip = signal<Ship>({
id: 1, name: 'Nazca', price: 10000
});
ships = signal<Ship[]>([]);
|
La prima riga di codice sopra crea un segnale numerico con un valore predefinito di 1. Poiché il valore predefinito è un numero, quantity è un segnale che contiene un numero. Quindi il parametro di tipo generico potrebbe essere anche omesso in questo caso:
1 |
quantity = signal(1);
|
La seconda riga è un segnale che contiene un array di numeri. L’impostazione predefinita fornisce una matrice di valori da 1 a 6. Anche in questo caso, il parametro di tipo generico non è necessario perché può essere dedotto dal valore predefinito.
Il segnale selectedShip
contiene un oggetto Ship
. In questo esempio, il tipo non può essere dedotto, quindi specifichiamo un parametro di tipo generico di Ship
.
Il segnale ships
contiene una serie di oggetti Ship
. Il suo valore predefinito è un array vuoto. Per riempire poi l’array, aggiungiamo un parametro di tipo generico di <Ship[]>
.
Un segnale creato con la funzione del costruttore del segnale è scrivibile, quindi puoi impostarlo su un nuovo valore, aggiornarlo in base al valore corrente o modificarne il contenuto. Vedremo esempi di queste operazioni a breve. Dopo aver creato un segnale, potresti voler leggere il suo valore.
Come leggere un Signal
1 |
quantity();
|
Inizia con il nome del segnale e poi ci sono le parentesi. Tecnicamente parlando, questo chiama la funzione getter del segnale. La funzione getter viene creata dietro le quinte: non la vedrai nel tuo codice.
Quando si lavora con Angular, un luogo comune per leggere i segnali è nel template.
1
2
3
4
5
6
7
8
9
10
|
<select
[ngModel]="quantity()"
(change)="onQuantitySelected($any($event.target).value)">
<option disabled value="">--Select a quantity--</option>
<option *ngFor="let q of qtyAvailable()">{{ q }}</option>
</select>
<div>Ship: {{ selectedShip().name}}</div>
<div>Price: {{ selectedShip().price | number: '1.2-2'}}</div>
<div style="font-weight: bold" [style.color]="color()">Total: {{ totalPrice() | number: '1.2-2' }}</div>
|
Il template sopra mostra una select box per la selezione di una quantità. Il [ngModel]
legge il valore del segnale di quantity
, legandosi a quel valore.
L’associazione dell’evento change
chiama il metodo onQuantitySelected()
nel componente.
L’elemento option
usa ngFor
per scorrere ogni elemento dell’array nel segnale qtyAvailable
. Legge il segnale e crea un’option
di selezione per ogni elemento dell’array.
Sotto l’elemento select ci sono tre elementi div. Il primo legge il segnale selectedShip
, quindi accede alla sua proprietà name. Il secondo elemento div legge il segnale selectedShip
, e fa visualizzare la proprietà price. L’ultimo elemento div legge il segnale totalPrice
(che non abbiamo ancora definito) e imposta il colore del testo sul valore del segnale color (che non abbiamo definito).
È importante notare che la lettura di un segnale legge sempre il valore corrente del segnale. Il codice non è a conoscenza di alcun valore di segnale precedente.
Quando l’utente seleziona una quantità diversa dall’elemento select, vogliamo modificare il valore del segnale quantity
. In questo modo il segnale quantity
diventa la “fonte della verità” per la quantità selezionata dall’utente. Diamo un’occhiata a come farlo dopo.
Come cambiare il valore a un Signal
Il metodo signal set
sostituisce il valore di un segnale con un nuovo valore.
1 |
this.quantity.set(qty);
|
Uno scenario comune consiste nel modificare il valore del segnale in base all’azione dell’utente. Per esempio:
- L’utente seleziona una nuova quantità utilizzando l’elemento select.
- L’evento nell’elemento select richiama il metodo
onQuantitySelected()
e trasmette la quantità selezionata. - L’azione dell’utente viene gestita all’interno del componente.
- Il nuovo valore viene impostato nel segnale
quantity.
Ecco un esempio sulla gestione dell’evento:
1
2
3
|
onQuantitySelected(qty: number) {
this.quantity.set(qty);
}
|
Se il codice legge un segnale, quel codice viene avvisato quando il segnale cambia.
Se un template legge un segnale, quel template riceve una notifica quando il segnale cambia e la view viene pianificata per essere nuovamente renderizzata.
Quindi l’atto di leggere un segnale registra il consumer interest nel guardare quel segnale. Il team di Angular definisce questa la regola d’oro dei componenti del segnale (signal components): “Il rilevamento delle modifiche (change detection) per un componente verrà pianificato quando e solo quando un segnale letto nel template notifica ad Angular che è stato cambiato”.
Se hai familiarità con RxJS e Observables, i segnali sono abbastanza diversi. I segnali non emettono valori come gli osservabili. E i segnali non richiedono una subscription.
Oltre a set()
, ci sono altri due modi per modificare un segnale: update()
e mutate()
.
Il metodo set()
sostituisce un segnale con un nuovo valore, sostituendo metaforicamente il contenuto del segnale. Passa il nuovo valore nel metodo set.
1
2
|
// Sostituisce il valore
this.quantity.set(qty);
|
Il metodo update()
aggiorna il segnale in base al suo valore corrente. Passa al metodo update un arrow function. L’arrow function fornisce il valore del segnale corrente in modo da poterlo aggiornare secondo necessità. Nel codice sottostante, la quantità è raddoppiata.
1
2
|
// Aggiorna il valore in base al valore corrente
this.quantity.update(qty => qty * 2);
|
Il metodo mutate()
modifica il contenuto di un valore del segnale, non il valore del segnale stesso. Usalo con gli array per modificare gli elementi dell’array e con gli oggetti per modificare le loro proprietà. Nel codice seguente, il prezzo di una nave è aumentato del 20%.
1 |
this.selectedShip.mutate((v) => v.price = v.price + (v.price * 0.2));
|
Indipendentemente da come il segnale viene modificato, i consumer vengono informati che il segnale è stato cambiato. I consumer possono quindi leggere il nuovo valore del segnale quando è il loro turno di esecuzione.
Come definire un Computed Signal
Spesso abbiamo variabili nel nostro codice che dipendono da altre variabili. Ad esempio, il prezzo totale di un articolo è il prezzo per quell’articolo moltiplicato per la quantità desiderata. Se l’utente cambia la quantità, cambia il prezzo totale. Per questa ragione si utilizzano i computed signal.
Anzitutto, si definisce un computed signal chiamando la funzione di creazione computed. La funzione computed()
crea un nuovo segnale che dipende da altri segnali.
Poi si passa alla funzione computed una funzione di calcolo che esegue l’operazione desiderata. L’operazione legge il valore di uno o più segnali per eseguire il suo calcolo.
1
2
|
totalPrice = computed(() => this.selectedShip().price * this.quantity());
color = computed(() => this.totalPrice() > 50000 ? 'red' : 'blue');
|
La prima riga di codice precedente definisce un computed signal totalPrice
chiamando la funzione di creazione computed()
. La funzione di calcolo passata a questa funzione calcolata legge i segnali selectedShip
e quantity
. Se uno dei segnali cambia, questo computed signal viene notificato e si aggiornerà quando sarà il suo turno di esecuzione.
La seconda riga di codice definisce un computed signal color
. Imposta il colore su rosso o blu a seconda del valore del segnale totalPrice
. Il template può essere associato a questo segnale per visualizzare lo stile appropriato.
Un computed signal è di solo lettura. Non può essere modificato con set()
, update()
o mutate()
.
Il valore di un computed signal viene ricalcolato quando:
- Uno o più segnali dipendenti sono cambiati.
- E il valore del computed signal viene letto.
Il valore del computed signal viene memorizzato, ovvero memorizza il risultato calcolato. Tale valore calcolato viene riutilizzato la volta successiva che viene letto il valore calcolato.
Come usare un Effect
Potrebbero esserci dei momenti in cui è necessario eseguire il codice quando un segnale cambia e ha degli effetti collaterali. Per effetti collaterali intendo codice che chiama un’API o esegue un’altra operazione non correlata al segnale. In questi casi, è meglio usare un effect()
.
Ad esempio, se vuoi eseguire il debug dei tuoi segnali e analizzare i loro comportamenti puoi usare la chiamata a console.log()
Per definire un effetto, bisogna chiamare la funzione di creazione effect()
. Passiamo alla funzione l’operazione da eseguire. Questa operazione viene rieseguita ogni volta che il codice reagisce a un cambiamento in qualsiasi segnale dipendente.
1 |
effect(() => console.log(JSON.stringify(this.ships())));
|
La funzione effect()
può essere chiamata all’interno di un’altra funzione. Poiché l’effetto imposta una sorta di gestore, viene spesso chiamato nel costruttore o in altro codice di avvio.
Un effetto non dovrebbe modificare il valore di alcun segnale. Se è necessario modificare un segnale è consigliabile utilizzare invece un computed signal.
Scoprirai che non userai spesso gli effetti, sebbene siano utili per il login o per chiamare altre API esterne. (ma non usarli per lavorare con RxJS e Observables. Ci saranno funzionalità dei segnali da convertire da e verso Observables.)
Quando usare i Signal
Innanzitutto, continua a utilizzare i gestori di eventi (event handlers) in un componente per le azioni dell’utente (user actions). Azioni come una selezione da un elenco a discesa, un clic su un pulsante o un inserimento su una casella di testo.
Puoi utilizzare un segnale o un computed signal in un componente per qualsiasi stato che tenderà a cambiare. In questo contesto, lo stato si riferisce a qualsiasi dato gestito dal componente. Tutto, da un flag isLoading
alla “pagina” di dati attualmente visualizzata, fino ai criteri di filtro selezionati dall’utente, potrebbe essere un segnale. I segnali sono particolarmente utili quando tali dati devono reagire ad altre azioni ed essere visualizzato in real-time.
Puoi inserire dei segnali condivisi nei servizi. Nel nostro esempio, la schiera di navi restituita in un Observable potrebbe essere trasformata in un segnale. Eventuali totali potrebbero anche essere dei segnali in un servizio, se tali segnali sono condivisi tra i componenti.
Continua però a usare gli Observables per le operazioni asincrone, cosi come per http.get()
. Ci sono più funzionalità in arrivo sui segnali per mappare un segnale da e verso un osservabile.
Conclusione
Un modo semplice per provare i segnali è utilizzare Stackblitz, un editor online che funziona bene con Angular e non richiede alcuna installazione. Per usare i segnali su Stackblitz, vai sul nostro codice online: https://stackblitz.com/edit/angular-signals-intro . Oppure scaricalo da Github, dalla nostra repository: https://github.com/Impesud/angular-signals-intro . Un follow o like sarà sempre gradito :)