Angular lifecycle e ExpressionChangedAfterItHasBeenCheckedError, perché accade e da cosa è causato?

Perchè in Angular la “ChangeDetection” può causare quest’ errore se modifico una proprietà nell’evento ngAfterViewInit()

Stefano Marchisio
8 min readJul 31, 2020
Angular lifecycle e ExpressionChangedAfterItHasBeenCheckedError

Alzi la mano chi tra voi si é imbattuto per lo meno una volta in quest’errore? Un nome così lungo, quando arrivi al fondo non ti ricordi più cosa c’era scritto all’inizio. A quel punto hai pensato: “Cosa caspita è successo?”

Perchè l’ Angular “ChangeDetection” può causare quest’ errore?

Introduzione

Bene, l’ obbiettivo del presente articolo è spiegare le cause del famigerato errore “ExpressionChangedAfterItHasBeenCheckedError” in cui spesso ci imbattiamo durante lo sviluppo. Ma visto che l’errore è legato al lifecycle di un Angular component , faremo prima una breve recall su alcuni concetti base.

Angular è un framework a componenti e all’interno del template HTML di un component è possibile dichiarare un altro componente. In questo modo si forma una struttura ad albero, componenti parent che componenti child. In Angular è anche presente un meccanismo di “ChangeDetection”, tramite il quale modificando una proprietà nel file .ts verrà aggiornato il template HTML (e qui entra in gioco il binding). Questo scatenerà poi un aggiornamento della UI e del DOM. Questo meccanismo di “ChangeDetection” deve (tra le altre cose) tenere in considerazione che esistono delle relazioni parent / child tra i componenti.

Il ciclo di vita di un Angular component(o component lifecycle) è scandita da eventi che possono essere scatenati una sola volta o più volte al verificarsi di determinate circostanze. Va inoltre detto (e si capirà meglio sotto) che il meccanismo di “ChangeDetection” avviene in modalità sincrona.

Per questo motivo all’ interno dei vari eventi di un componete esistono cose che si POSSONO FARE e cose che NON SI POSSONO FARE. Così come esistono cose che possono essere fatte in maniera SINCRONA come cose che devono essere fatte in maniera ASINCRONA!

Per informazioni più dettagliate su, fare riferimento alla documentazione.

Premesso ciò introduciamo ora il famigerato errore (che era il tema del presente articolo 😀).

ExpressionChangedAfterItHasBeenCheckedError

In Angular oltre agli errori di compilazione abbiamo anche gli errori runtime, che possiamo vedere all’interno della console del browser (tanto per capirci quella a cui si accede tramite F12). Quest’errore compare appunto nella console del browser; anche se molte volte l’applicazione continua a funzionare c’è quest’errore che provoca in noi una certa irritazione.

Angular ExpressionChangedAfterItHasBeenCheckedError

La difficoltà maggiore quando ci imbattiamo in quest’errore, è capire da cosa è generato (l’introduzione appena fatta poco sopra ci viene ora in aiuto!). Per rendere le cose più semplici diamo prima una risposta breve e poi all’interno di una trattazione più lunga entreremo dei dettaglio e vedremo da cosa è generato e cosa c’è a monte.

Risposta breve

ExpressionHasChangedAfterChecked — Insorge nel momento in cui proviamo a modificare qualche cosa (per esempio una proprietà di binding) all’ interno dell’evento ngAfterViewInit(). Questo perché il check può essere ancora in corso.

Possiamo riscontrare quest’errore anche quando un componente figlio prova a modificare proprietà del parent e viceversa. Stiamo modificando una proprietà in un momento non opportuno. Ovvero è in corso il controllo delle modifiche.

La prima cosa che può venire in mente è: “ma allora perché usiamo l’evento ngOnInit() per modificare una proprietà”. Purtroppo alcune cose non sono ancora disponibili nell’evento ngOnInit() e dobbiamo per forza farle nell’evento ngAfterViewInit().

Da notare che poco sopra abbiamo detto che la “ChangeDetection” avviene in modalità sincrona, pertanto nel momento in cui scatta l’evento ngAfterViewInit(), la “ChangeDetection” può essere ancora in corso!

Risposta lunga (e component lifecycle)

Vediamo ora la risposta lunga. Per capire cosa c’è alla base dobbiamo prima introdurre alcune funzionalità di base di Angular già accennate poco sopra.

Ciclo di vita di un componente Angular (o component lifecycle)

Abbiamo detto che Angular è un framework a componenti, per questo motivo ogni componente ha degli eventi che vengono scatenati in determinati momenti (sotto elencheremo i vari eventi e quando vengono scatenati).

Angular component lifecycle

1) ngOnChanges(changes: SimpleChanges) - Viene invocato all’inizio passando il valore iniziale di ogni proprietà @Input() ed in futuro ad ogni cambiamento. Da tener presente che la comparazione viene fatta per referenza, per questo motivo se modifico una proprietà interna di un oggetto la cui referenza è sempre la stessa, quest’evento non scatterà. Quest’evento ha un argomento “SimpleChanges”, in questo oggetto troveremo le proprietà di @Input() ed i relativi valori (precedente e corrente). Se ci serve solo il valore iniziale di una proprietà possiamo prenderlo direttamente dalla proprietà dopo l’evento “ngOnInit()” senza registraci a quest’evento. L’evento ngOnChanges() scatta all’inizio ed in seguito al variare di ogni proprietà.

2) ngOnInit() - Quest’evento indica che il componente Angular è stato creato e tutte le proprietà di @Input() sono state inizializzate con il valore iniziale. La differenza tra quest’evento ed il costruttore è che: il costruttore è un metodo che viene eseguito quando la classe viene istanziata e garantisce la corretta inizializzazione delle proprietà nella classe e nelle sue sottoclassi (Dependency Injector). Da tener presente che all’interno del costruttore non sono ancora state inizializzate le proprietà di @Input() con il valore iniziale. L’evento ngOnInit() scatta una sola volta.

3) ngDoCkeck() - Chiamato all’inizio immediatamente dopo ngOnInit() e ngOnChanges() e ad in seguito ad ogni esecuzione del rilevamento delle modifiche o change-detection.

4) ngAfterContentInit() - Chiamato dopo che il contenuto esterno è stato projected nel componente. Eventuali query con @ContentChild() e @ContentChildren() sono impostate prima che quest’evento venga chiamato. L’evento ngAfterContentInit() scatta una volta dopo il primo ngDoCheck().

5) ngAfterContentChecked() - Chiamato dopo ogni controllo del contenuto projected del componente. L’evento ngAfterContentChecked() scatta dopo ngAfterContentInit() e ogni successivo ngDoCheck().

6) ngAfterViewInit() - Chiamato dopo che il componente e i relativi figli sono stati inizializzati. L’evento ngAfterViewInit() scatta una volta dopo il primo ngAfterContentChecked().

7) ngAfterViewChecked() - Chiamato dopo ogni controllo del componente e dei relativi figli. L’evento ngAfterViewChecked() scatta dopo ngAfterViewInit() e ogni successivo ngAfterContentChecked().

La prima cosa che può venire in mente ora è: “ma perché usiamo l’evento ngOnInit() per modificare una proprietà”. Purtroppo alcune cose non sono ancora disponibili nell’evento ngOnInit() e dobbiamo per forza farle nell’evento ngAfterViewInit(). Sotto spieghiamo il perché.

“Template Reference Variable” e @ViewChild() / @ContentChild()

Introduciamo ora un altro importante concetto di Angular. Una “Template Reference Variable” è una referenza ad un elemento all’interno di un template HTML, e con elemento si intende sia un semplice elemento HTML che un altro componente o direttiva presente nel template HTML. Nel primo caso avremo una referenza ad un elemento del DOM (ed il tipo associato è ElementRef) nel secondo caso una referenza ad un componente o direttiva. Se dall’interno del componente vogliamo ottenere una referenza a tale elemento dobbiamo fare una query utilizzando @ViewChild() o @ContentChild().

Esistono 2 tipi di query: le query statiche e le query dinamiche. Questo dipende da quando il risultato della query sarà disponibile. Nella documentazione di Angular troviamo la seguente definizione:

With static queries (static: true), the query resolves once the view has been created, but before change detection runs. The result, though, will never be updated to reflect changes to your view, such as changes to ngIf and ngFor blocks.

With dynamic queries (static: false), the query resolves after either ngAfterViewInit() or ngAfterContentInit() for @ViewChild() and @ContentChild() respectively. The result will be updated for changes to your view, such as changes to ngIf and ngFor blocks.

Se vogliamo fare una query statica dobbiamo passare come parametro al decoratore @ViewChild({ static: true }). Se vogliamo fare una query dimamica dobbiamo passare come parametro al decoratore @ViewChild({ static: false }). Di default se non passiamo niente facciamo una query dinamica ovvero { static: false }. Questo dalla versione di Angular 9 in poi!

In base a ciò che è stato detto poco sopra, per avere una referenza di un componente figlio possiamo essere costretti a farlo solo nel evento ngAfterViewInit(), ma se poi proviamo a modificare una proprietà del componente figlio? BUM … ExpressionHasChangedAfterChecked.

(ExpressionHasChangedAfterChecked) insorge nel momento in cui proviamo a modificare qualche cosa (per esempio una proprietà di binding) all’ interno dell’evento ngAfterViewInit(). Questo perché il check può essere ancora in corso.

Possiamo riscontrare quest’errore anche quando un componente figlio prova a modificare proprietà del parent e viceversa. Stiamo modificando una proprietà in un momento non opportuno. Ovvero è in corso il controllo delle modifiche.

Come risolvere il problema

In base a ciò che abbiamo visto fino ad ora, possiamo dire che l’errore può capitare nei seguenti contesti:

1) Si sta eseguendo codice nell’evento AfterViewInit(), spesso accade quando si fanno delle query @ViewChild() / @ContentChild() che possono non essere disponibili fino a quando non viene chiamato l’evento AfterViewInit(). Piuttosto che anche solo modificare una proprietà di binding (spiegato sopra).

2) Si sta manipolando il DOM diretamente (ad es. usando jQuery). Angular non è in grado di rilevare questi cambiamenti e reagire correttamente.

3) Può anche accadere se si chiamano funzioni all’interno del template HTML.

Possiamo anche imbatterci in quest’errore in debug ma non in produzione, questo perché Angular in debug (a scopo di verifica) fa scattare il rilevamento delle modifiche (o change detection 2 volte). Se nel template abbiamo una funzione che ritorna valori diversi (per esempio una data), avremo l’errore.

Non esiste un’unica soluzione a questo problema, comunque si può provare ad usare: setTimeout o ChangeDetectorRef. Comunque all’interno AfterViewInit() non modificare una proprietà in modo sincrono, ma bensì in modo asincrono (visto che la “ChangeDetection” è essa stessa sincrona).

Introduciamo ora l’event-loop di JavaScript

JavaScript è un linguaggio asincrono e single-thread, senza entrare nel dettaglio, questo avviene grazie all’event-loop illustrato dall’immagine sottostante. Maggiori informazioni sull’event-loop.

JavaScript event-loop

Se proviamo a modificare qualche cosa in modo asincrono, per esempio tramite una chiamata dell’ HttpClient; dopo aver effettuato la chiamata sarà all’event-loop a rimanere in attesa della risposta, ed il codice iniziale finisce tranquillamente. Quando il thread sarà libero event-loop invierà l’eventuale risposta. Se la risposta arriva prima event-loop rimarrà in attesa che il thread si liberi. Event-loop essendo un componente della JavaScript VM ha un suo thread, diverso da quello in cui è in esecuzione il JavaScript che è uno solo.

In questo modo eventuali modifiche saranno fatte quando la “ChangeDetection” è terminata; a questo punto un’eventuale modifica di una proprietà farà scattare nuovamente la “ChangeDetection”.

Questo perché la “ChangeDetection” scatta al verificarsi di 3 situazioni: eventi ui, setTimeout() / setInterval() e operazioni ajax (zone.js).

Conclusioni

La modifica di una proprietà, per essere sicuri di non incorrere nell’errore, deve essere fatta in modo asincrono

In questo modo modificheremo la proprietà NEL MOMENTO OPPORTUNO, ovvero senza avere un ciclo di “ChangeDetection” in corso!

Per informazioni più dettagliate su, fare riferimento alla documentazione.

Se volete contattarmi il mio profilo Linkedin è il seguente: Stefano Marchisio: Consulente freelance Angular ASP.NET MVC C#

--

--