
Con l’introduzione dei Signals in Angular, la gestione dello stato è diventata molto più chiara, manutenibile e prevedibile.
Grazie alle primitive come signal()
, computed()
ed effect()
, possiamo modellare e derivare i dati in modo efficace, mantenendo sotto controllo le dipendenze tra i vari pezzi di stato.
Tuttavia, aggiornare i valori derivati non è altrettanto immediato e richiede spesso interventi manuali sullo stato principale.
Per rispondere a questa esigenza il team di Angular sta lavorando ad una nuova primitiva: Deep Signal.
Si tratta di una soluzione robusta per accedere e modificare direttamente, in modo reattivo, le proprietà annidate all’interno di un altro Signal.
In questo articolo vedremo cosa sono i Deep Signal, come funzionano e perché possono semplificare il codice delle nostre applicazioni Angular.
🎯 Accedere e aggiornare lo stato annidato
Consideriamo il caso di un’applicazione che gestisce i dati anagrafici di un utente. Uno scenario molto comune, in cui lo stato applicativo racchiude informazioni come nome, cognome e città di residenza:
const userModel = signal({
name: 'Davide',
lastname: 'Passafaro',
city: 'Rome'
}
});
Code language: TypeScript (typescript)
Grazie ai computed()
Signal, possiamo facilmente derivare valori da questo stato, ad esempio per ottenere il nome e la città dell’utente:
const userName = computed(() => userModel().name);
const userCity = computed(() => userModel().city);
Code language: TypeScript (typescript)
Supponiamo ora di voler consentire all’utente di aggiornare le proprie informazioni tramite un form. Grazie ad ngModel
, possiamo collegare direttamente gli input del form ai valori dei nostri Signals:
<p>Nome: <input type="text" [(ngModel)]="userName()"></p>
<p>Città: <input type="text" [(ngModel)]="userCity()"></p>
Code language: HTML, XML (xml)
Tuttavia, qui sorge un problema: i valori derivati da computed()
sono solamente in lettura e quindi non possono essere aggiornati direttamente.
La direttiva ngModel
, invece, si aspetta di poter sia leggere che scrivere sul modello associato.
Linked Signal: un workaround, non proprio una soluzione
Per aggiornare una proprietà annidata, siamo costretti a intervenire manualmente sullo stato padre, creando una funzione dedicata.
Nel nostro esempio, possiamo definire una funzione updateCity
come questa:
function updateCity(newCity: string) {
userModel.update(user => ({ ...user, city: newCity }));
}
Code language: TypeScript (typescript)
A questo punto, colleghiamo poi questa funzione all’input del form, gestendo esplicitamente l’evento di modifica:
<p>
Città:
<input
type="text"
[ngModel]="userCity()"
(ngModelChange)="updateCity($event)"
/>
</p>
Code language: HTML, XML (xml)
Questa soluzione, seppur funzionale, non è reattiva di per sé: si basa su una chiamata esplicita della funzione updateCity(), lasciando a noi il compito di mantenere la coerenza dello stato.
E, come sappiamo, ciò non avviene sempre in modo sistematico.
Per aggirare questa limitazione, possiamo utilizzare linkedSignal()
: una funzione che consente di creare un Signal derivato ma anche scrivibile.
Combinando il tutto con un effect()
, possiamo ottenere una sorta di sincronizzazione bidirezionale tra il valore derivato e lo stato principale:
const userModel = signal({
name: 'Davide',
lastname: 'Passafaro',
city: 'Rome'
}
});
const userCity = linkedSignal(
() => () => userModel().city
);
effect(() => {
userModel.set({...userModel(), city: userCity())
});
Code language: HTML, XML (xml)
In questo scenario, il valore di userCity()
è derivato da userModel()
, ma può anche essere aggiornato direttamente tramite userCity.set()
.
Il nostro form diventa molto più lineare:
<p>Nome: <input type="text" [(ngModel)]="userName()"></p>
<p>Città: <input type="text" [(ngModel)]="userCity()"></p>
Code language: HTML, XML (xml)
Ogni modifica a userCity()
viene intercettata dall’effect()
, che aggiorna di conseguenza lo stato principale. Otteniamo così una sincronizzazione bidirezionale manuale fatta in casa.
Splendido!!!
Beh, non proprio…
Risolviamo sì il problema, ma al prezzo di una maggiore complessità:
- Perché utilizziamo
linkedSignal()
al posto di un semplicecomputed()
? - Come mai serve quell’
effect()
?
Queste sono tutte domande che i prossimi manutentori del codice, o i noi stessi del futuro, potrebbero porsi, o ritrovandosi davanti a un codice poco chiaro che non racconta chiaramente il “perché” delle scelte fatte.
Per questo motivo, serve una soluzione più robusta e nativamente bidirezionale, che non richieda workaround manuali e che sia chiara ed esplicita anche per chi leggerà o manterrà il codice in futuro.
Ed è proprio qui che entrano in gioco i Deep Signals.
🧬 La nuova primitiva Deep Signal
Con la nuova primitiva Deep Signal, Angular introduce una soluzione robusta e dichiarativa alla gestione degli stati derivati.
Un Deep Signal consenti infatti di accedere e modificare direttamente, in modo reattivo e bidirezionale, proprietà interne ad un signal()
.
Come si crea un Deep Signal
Come per le altre primitive, anche per creare un Deep Signal, Angular mette a disposizione una funzione dedicata deepSignal()
:
const userModel = signal({
name: 'Davide',
lastname: 'Passafaro',
city: 'Rome'
}
});
const userCity = deepSignal(userModel, 'city');
Code language: HTML, XML (xml)
La funzione deepSignal()
accetta due parametri: il Signal padre contenente lo stato da cui vogliamo estrarre una proprietà, e la chiave (key) che identifica quella proprietà all’interno della struttura, espressa come stringa o come Signal che contiene una stringa.
Il valore ottenuto da deepSignal()
è a tutti gli effetti un Signal bidirezionale: possiamo quindi leggerne il valore come faremmo con un normale computed()
, ma anche aggiornarlo direttamente tramite il metodo set()
o update()
, senza dover ricostruire manualmente lo stato padre.
Nel nostro esempio:
console.log(userCity()); // Prints "Rome"
userCity.set('Turin'); // Updates userModel().city
Code language: TypeScript (typescript)
E naturalmente, essendo un Signal, può essere utilizzato senza problemi anche nei template:
<p>Città: <input type="text" [(ngModel)]="userCity()"></p>
Code language: HTML, XML (xml)
La direttiva ngModel
è ora perfettamente compatibile: ogni modifica al campo input aggiornerà direttamente lo stato annidato all’interno di userModel()
, e viceversa.
Questo approccio riduce drasticamente il boilerplate necessario e migliora la leggibilità e la coerenza del codice, rendendo le operazioni di accesso e aggiornamento su porzioni di stato annidate più dirette, esplicite e sicure.
Performance dei Deep Signals
I Deep Signal non sono solo più comodi da usare, ma portano con se anche vantaggi sostanziali in termini di performance.
Come abbiamo visto in precedenza, senza di essi, l’aggiornamento di una proprietà annidata richiede l’aggiornamento del Signal padre.
const userModel = signal({
name: 'Davide',
lastname: 'Passafaro',
city: 'Rome'
}
});
userModel.set({...userModel(), city: 'Turin')
Code language: TypeScript (typescript)
Questo comporta che ogni modifica anche minima, ad esempio aggiornare solo la proprietà city
, scatena l’elaborazione di tutti gli effetti o i componenti che dipendono anche solo in parte dallo stato completo userModel
.
In pratica, anche chi è interessato solo al name
o al lastname
verrà notificato e ogni stato derivato verrà ricalcolato. Questi rileveranno poi che i dati a cui sono legati non sono effettivamente cambiati, e quindi eviteranno di propagare ulteriori notifiche di aggiornamento.
Nonostante ciò, il costo in termini di prestazioni resta significativo, soprattutto in applicazioni con stati complessi o numerosi effetti collegati.
Deep Signal risolve questo problema grazie a una gestione più granulare e mirata delle dipendenze: aggiorna direttamente le proprietà annidate, notificando solo chi è in ascolto di quella specifica porzione di stato.
In questo modo, solo gli effetti e i componenti effettivamente interessati alla proprietà modificata vengono aggiornati, riducendo drasticamente il numero di notifiche e ricalcoli superflui, con evidenti benefici sulle prestazioni.
Ma non è finita qui.
🧱 Structural Signals: quando la struttura conta
L’introduzione dei Deep Signals apre la porta ad un altra primitiva Signal molto utile: Structural Signals.
Uno Structural Signals viene derivato da un altro WritableSignal, che ne restituisce il valore completo come farebbe un Computed Signal con questa forma:
const userModel = signal({
name: 'Davide',
lastname: 'Passafaro',
city: 'Rome'
}
});
const computedUserModal = computed(() => userModel());
Code language: TypeScript (typescript)
L’unica differenza significativa è che uno Structural Signal non viene notificato quando i cambiamenti avvengono tramite un Deep Signal.
Ciò significa che uno Structural Signal rileva le modifiche solo quando il Signal padre viene aggiornato direttamente.
Combinando con attenzione Structural Signal e Deep Signal, possiamo dunque ottenere un controllo più granulare e intelligente sugli aggiornamenti, evitando trigger inutili causati da nuove referenze che in realtà non modificano i contenuti.
const userModel = signal({
name: 'Davide',
lastname: 'Passafaro',
city: 'Rome'
});
// Structural Signal: returns the entire object
const userSnapshot = structuralSignal(userModel);
// Deep Signal: accesses a specific nested property
const userCity = deepSignal(userModel, 'city');
// Effect that reacts only to full structural changes
effect(() => {
console.log('🧱 Structural update:', userSnapshot());
});
// Effect that reacts only to changes in the city property
effect(() => {
console.log('🌍 City changed:', userCity());
});
// Update only the city: triggers only the Deep Signal effect
userCity.set('Milan');
// Output:
// 🌍 City changed: Milan
// Structural update (new object): triggers both effects
userModel.set({
name: 'Davide',
lastname: 'Passafaro',
city: 'Turin'
});
// Output:
// 🌍 City changed: Turin
// 🧱 Structural update: { name: 'Davide', lastname: 'Passafaro', city: 'Turin' }
Code language: TypeScript (typescript)
In questo esempio, possiamo osservare un comportamento preciso:
- Se modifichiamo solo
userCity
tramite il Deep Signal, verrà notificato solo chi ascolta quella proprietà (come il primoeffect()
); - Se invece sostituiamo l’intero oggetto
userModel
, verranno notificati anche gli osservatori strutturali (come il secondoeffect()
).
✅ Considerazioni finali
L’introduzione dei Deep Signals e degli Structural Signals rappresenta una aggiunta interessante al mondo Angular.
Poter aggiornare uno stato derivato all’interno di un Signal in modo chiaro e mirato, rappresenta un grande passo avanti per la leggibilità del codice, la manutenibilità e le performance delle applicazioni.
Nonostante ciò, manca ancora qualcosa.
Attualmente, i Deep Signal supportano solo proprietà annidate ad un singolo livello di profondità. Non è ancora possibile accedere in modo reattivo a proprietà più profonde o a stati derivati da computazioni più complesse.
Queste limitazioni non tolgono valore alle nuove primitive, ma sono un chiaro segnale (badum-tss 🥁) della direzione che Angular sta prendendo. Con ogni nuovo rilascio sta diventando sempre più potente e flessibile.
A noi non resta che aspettare, pronti a sperimentare le prossime novità.
Link utili per approfondire
Grazie per aver letto questo articolo 🙏
Mi piacerebbe avere qualche feedback quindi grazie in anticipo per qualsiasi commento. 👏
Infine, se ti è piaciuto davvero tanto, condividilo con la tua community. 👋😁