
GUIDE PER ASPIRANTI PROGRAMMATORI
Lifecycle Hooks in Angular
Nel corso di questa guida abbiamo più volte menzionato e utilizzato l’interfaccia OnInit e il metodo ngOnInit ad essa associato. In questa sezione della guida approfondiremo i cosiddetti lifecycle hooks (letteralmente ganci del ciclo di vita o agganci al ciclo di vita) e vedremo esattamente a cosa servono e come usarli al meglio. Se ci…


Vuoi avviare una nuova carriera o fare un upgrade?
Trova il corso Digital & Tech più adatto a te nel nostro catalogo!
- Le direttive in Angular
- I componenti in Angular
- Il template in Angular
- Le direttive strutturali in Angular
- La content projection in Angular
- I servizi in Angular
- Le Pipes in Angular
- Routing in Angular
- Invio di form in Angular
- Built-in control flow in Angular
- Deferrable views in Angular
- Image optimization in Angular
- Standalone components in Angular
Nel corso di questa guida abbiamo più volte menzionato e utilizzato l’interfaccia OnInit e il metodo ngOnInit ad essa associato. In questa sezione della guida approfondiremo i cosiddetti lifecycle hooks (letteralmente ganci del ciclo di vita o agganci al ciclo di vita) e vedremo esattamente a cosa servono e come usarli al meglio.
Se ci fermassimo al solo ngOnInit, avremmo una certa sovrapposizione di scopo tra questo metodo e il costruttore della classe componente (o direttiva, o pipe); in realtà l’unica cosa in cui questi due “metodi” si somigliano è il fatto di essere chiamati all’inizio del ciclo di vita, ma ci sono comunque delle importanti differenze tra i due rispetto al loro rapporto con il framework.
Tanto per cominciare, il costruttore viene eseguito una sola volta, mentre nulla ci vieta di chiamare ngOnInit da codice per reinizializzare lo stato del componente; inoltre, poiché Angular deve inizializzare le input property in base agli attributi popolati nel template del componente padre, il metodo ngOnInit ha anche l’importante funzione di fornire un entrypoint del componente completo di tutti gli input:
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-input', standalone: true, template: 'Works' }) export class InputComponent { @Input() input!: string; constructor() { console.log(this.input) } // undefined ngOnInit() { console.log(this.input) } // hello } @Component({ selector: 'app-root', standalone: true, imports: [InputComponent], template: '<app-input input="hello" />', }) export class AppComponent {}
In questo esempio InputComponent logga in console prima undefined, poi hello; dunque, è meglio usare il costruttore solo per inizializzare campi con valori statici o per dichiarare le dipendenze del componente, oppure non usarlo affatto, dichiarando le proprietà in fase di dichiarazione e usando l’utility inject per ottenere le dipendenze.
Notiamo anche che in questo esempio il metodo ngOnInit è stato utilizzato senza implementare esplicitamente l’interfaccia OnInit; in realtà, poiché le interfacce esistono solo a livello TypeScript, dichiarare l’interfaccia associata ad ogni lifecycle hook è una buona pratica ma non un obbligo. Di seguito, faremo riferimento agli hook parlando dei metodi e non delle interfacce ad essi associate, che comunque seguono tutte la stessa nomenclatura.
Il gancio opposto rispetto a on init è ovviamente on destroy; questo hook viene chiamato ogni volta che un componente viene rimosso dalla pagina, per esempio per via di un blocco @if oppure perché l’app ha navigato in un’altra vista.
Lo scopo di ngOnDestroy è quello di consentirci di chiudere eventuali sottoscrizioni a eventi o Observables, o di arrestare eventuali interval o timeout necessari per il funzionamento del componente.
Prendiamo il seguente esempio:
import { Component, OnDestroy, OnInit } from '@angular/core'; const listener = (e: MouseEvent) => console.log('mouse event', e); @Component({ selector: 'app-log-click', standalone: true, template: '' }) export class LogClickComponent implements OnInit, OnDestroy { ngOnInit(): void { document.addEventListener('click', listener) } ngOnDestroy(): void { /* document.removeEventListener('click', listener)*/ } } @Component({ selector: 'app-root', standalone: true, imports: [LogClickComponent], template: ` @if (logClick) { <app-log-click /> } <pre>{{logClick}}</pre> <button (click)="toggle()">Toggle</button> `, }) export class AppComponent { logClick = false; toggle() { this.logClick = !this.logClick } }
Abbiamo un componente LogClickComponent che non ha alcun template ma si occupa di registrare un event listener su document, cioè sull’intera pagina. AppComponent mostra o nasconde il componente nel suo template in base al booleano logClick. Poiché la logica dentro ngOnDestroy è commentata, anche quando rimuoviamo il componente la pagina continua a loggare i nostri click.
Dal momento che nei moderni linguaggi di programmazione non siamo abituati a preoccuparci di ripulire manualmente la memoria, errori come questo sono molto frequenti e possono causare comportamenti indesiderati e grossi impatti sulle performance della nostra app. In generale, dovunque c’è un OnInit dovremmo chiederci se è anche necessario un OnDestroy; l’unico caso in cui possiamo essere indulgenti al riguardo è proprio in AppComponent che, rappresentando l’intera applicazione, viene distrutto insieme alla finestra del browser che la esegue.
Abbiamo detto che ngOnInit viene chiamato dopo l’inizializzazione delle input properties; nel processo di inizializzazione del componente vi sono altri due potenziali passaggi, vale a dire l’inizializzazione di ciò che è rappresentato nel template del componente (view) e di ciò che è rappresentato come figlio del tag del componente e proiettato attraverso ng-content (content).
Questi due passaggi sono rappresentati dai ganci ngAfterViewInit e ngAfterContentInit.
Questi due hook sono specialmente utili se usati insieme ai decoratori @ViewChild/@ViewChildren e @ContentChild/@ContentChildren, come nell’esempio:
import { AfterContentInit, AfterViewInit, Component, ContentChild, ElementRef, ViewChild } from '@angular/core'; @Component({ selector: 'app-sample', standalone: true, template: '<p #view>View</p><ng-content />' }) export class SampleComponent implements AfterViewInit, AfterContentInit { @ViewChild('view') view!: ElementRef<HTMLParagraphElement>; @ContentChild('content') content!: ElementRef<HTMLParagraphElement>; ngAfterViewInit(): void { console.log('view init', this.view) } ngAfterContentInit(): void { console.log('content init', this.content) } } @Component({ selector: 'app-root', standalone: true, imports: [SampleComponent], template: '<app-sample><p #content>Content</p></app-sample>', }) export class AppComponent {}
Qui abbiamo usato nello stesso esempio sia view sia content, con i decoratori associati ai rispettivi hook.
Il funzionamento di @ViewChild e affini è simile a quello del nativo document.querySelector, ma invece dei soliti selettori CSS accetta come argomento il nome di una template variable oppure il tipo di una direttiva o di un componente; in questo modo Angular è in grado di fornirci direttamente l’istanza del componente a cui siamo interessati invece del semplice tag HTML.
Per quanto riguarda gli omologhi multipli @ViewChildren/@ContentChildren, questi decorano delle proprietà di tipo QueryList, delle semplici strutture dalle quali possiamo estrarre i nodi a cui siamo interessati oppure ciclare su di essi.
Gli hook visti fin qui vengono chiamati tutti una volta sola da Angular, ed eventualmente altre volte dal nostro codice; i seguenti hook vengono invece chiamati più frequentemente nel corso della vita del componente, e dunque dovremo fare attenzione a non appesantirli troppo per non impattare eccessivamente sulle performance:
- AfterView/ContentChecked: omologhi degli hook appena visti, vengono chiamati ogni volta che la view e il content vengono aggiornati; si raccomanda di usare questi due hook solo se strettamente necessario poiché il loro impatto sulle prestazioni è significativo;
- DoCheck: viene chiamato prima che Angular aggiorni la vista, dandoci l’opportunità di aggiungere manualmente delle logiche di aggiornamento della vista in seguito a modifiche dello stato; questo hook ha casi d’uso estremamente limitati e strettamente tecnici;
- OnChanges: questo hook viene chiamato ogni volta che le input property vengono aggiornate, e gli viene passato un parametro che contiene una mappa delle modifiche; si rivela molto utile in quei componenti, direttive o pipes che hanno bisogno di far discendere delle operazioni ad ogni aggiornamento dei loro input.
CONTENUTI GRATUITI IN EVIDENZA
Guide per aspiranti programmatori 👨🏻🚀
Vuoi muovere i primi passi nel Digital e Tech? Abbiamo preparato alcune guide per aiutarti a orientarti negli ambiti più richiesti oggi.