back to top

Il ciclo di vita di un componente in Angular

In questa lezione illustreremo quali sono le diverse fasi del ciclo di vita che attraversa un componente dal momento in cui viene creato fino a quando viene distrutto. Angular dà la possibilità di intercettare queste fasi per avere un controllo maggiore su ciascun componente. Per far ciò basterà aggiungere ai singoli componenti alcuni metodi particolari che prendono il nome di Lifecycle hooks i quali verranno opportunamente invocati da Angular in base alla fase in cui si trova il componente durante il suo ciclo di vita.

Quali sono i diversi Lifecycle hooks di un componente

Angular mette a disposizione 8 Lifecycle hooks ognuno dei quali viene invocato in un fase diversa del ciclo di vita di un componente.

  • ngOnChanges
  • ngOnInit
  • ngDoCheck
  • ngAfterContentInit
  • ngAfterContentChecked
  • ngAfterViewInit
  • ngAfterViewChecked
  • ngOnDestroy

Prima di parlare di ciascuno dei metodi riportati sopra e capire in che modo poterli utilizzare al meglio, soffermiamoci per un momento sul costruttore di un componente anche se tecnicamente non rientra nella categoria dei Lifecycle hooks.

Il costruttore di un componente non è una funzionalità specifica di Angular, ma è il metodo predefinito che viene utilizzato per creare un’istanza di una classe in TypeScript. Viene invocato una sola volta prima che vengano eseguiti i metodi specifici del ciclo di vita del componente. Per questo motivo all’interno del costruttore non è possibile accedere agli elementi presenti nel template o ad altri elementi del DOM. Anche le proprietà di input, usate per passare delle informazioni a un componente, non sono state ancora inizializzate e se si prova ad accedere al loro valore, verrà restituito semplicemente undefined.

Il costruttore viene solitamente sfruttato per iniettare dei servizi all’interno del componente come vedremo quando parleremo del meccanismo di Dependency Injection e può essere usato per inizializzare le proprietà locali del componente. In generale è sconsigliato effettuare operazioni che richiedano molto tempo e risorse per essere completate. Allo stesso modo, se abbiamo bisogno di recuperare dei dati da un server remoto, il costruttore non è il metodo ideale per farlo visto che a tale scopo è consigliato usare il Lifecycle hook ngOnInit. In questo modo non ci sarà nemmeno il rischio che un nuovo componente tenti di contattare un server remoto quando viene creato all’interno dei test di unità.

Come gestire le fasi del ciclo di vita di un componente attraverso i Lifecycle hooks

Prima di esplorare i diversi Lifecycle hooks vediamo come intercettare una delle fasi del ciclo di vita di un componente illustrando un esempio in cui definiamo il metodo ngOnInit. Il motivo della nostra scelta è dovuto al fatto che è sicuramente uno dei metodi più utilizzati e l’abbiamo già incontrato in qualche esempio delle precedenti lezioni. Il procedimento è simile per tutti gli altri metodi. I passi da seguire sono essenzialmente 3:

  1. Importare la rispettiva interfaccia da @angular/core che coincide col nome del metodo al quale però va levato il prefisso ‘ng’. Per cui per il metodo ngOnInit dovremo importare l’interfaccia OnInit().
  2. Assicurarci che il componente implementi l’interfaccia importata.
  3. Definire opportunamente il metodo attraverso il quale possiamo indicare che tipo di operazione eseguire ad un certo istante del ciclo di vista di un componente.

Angular CLI semplifica ancora una volta il lavoro e nel caso particolare del metodo ngOnInit(), esegue i passaggi descritti sopra in automatico quando impartiamo il comando ng generate component.

Esempio Lifecycle hook in un componente Angular

Il metodo ngOnInit()

Iniziamo a parlare di ngOnInit che viene invocato in fase di inizializzazione di un componente dopo che è stata terminata l’esecuzione del costruttore e di ngOnChanges() che è il primo metodo del ciclo di vita ad viene eseguito quando viene creato un nuovo componente. Dal momento che ngOnChanges() viene invocato prima di ngOnInit(), in quest’ultimo abbiamo accesso a eventuali proprietà di input al contrario di quanto accade nel costruttore. Il metodo ngOnInit() viene comunque invocato prima di inizializzare qualsiasi proprietà di eventuali componenti figli.

import { ChildComponent } from './child.component';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'simple-parent-component',
  template: `
    <simple-child-component [message]="greeting"></simple-child-component>
  `
})
export class ParentComponent implements OnInit {
  greeting = 'ciao';

  constructor() {
    console.log('+ Parent component: constructor');
  }
  ngOnInit() {
    console.log('+ Parent component: ngOnInit');
  }
}
import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'simple-child-component',
  template: `
    <p>
      Messaggio ricevuto dal padre: {{ message }}
    </p>
  `,
  styles: []
})
export class ChildComponent implements OnInit {

  @Input() message: string;

  constructor() {
    console.log('>> ChildComponent: constructor');
    console.log('>> ChildComponent: valore messaggio' +
      'ricevuto nel costruttore ->', this.message
    );
  }

  ngOnInit() {
    console.log('>> ChildComponent: ngOnInit');
    console.log('>> ChildComponent: valore messaggio' +
      'ricevuto in ngOnInit ->', this.message
    );

}
output console esempio ngOnInit

Il metodo ngOnInit può essere usato per recuperare da un server remoto dei dati necessari al funzionamento del componente o dei suoi discendenti, per inizializzare delle proprietà che necessitano delle informazioni ricevute tramite le proprietà di input oppure per operazioni di configurazione iniziale che non si vogliono effettuare nel costruttore perché sono più complesse.

Il metodo ngOnDestroy()

Il metodo ngOnDestroy() viene eseguito una sola volta immediatamente prima di distruggere il componente. All’interno di questo Lifecycle hook abbiamo la possibilità di liberare le risorse che non vengono automaticamente eliminate dal Garbage collector. Questo è il metodo ideale per cancellare dei timer, invocare il metodo unsubscribe() di eventuali istanze di Observable o rimuovere associazioni tra eventi e handler attraverso la funzione removeEventListener().

import { ChildComponent } from './child.component';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'simple-parent-component',
  template: `
    <div>
      <simple-child-component *ngIf="!hidden" [message]="greeting">
      </simple-child-component>
    </div>
    <button (click)="hidden = !hidden">
      {{ hidden ? 'Mostra' : 'Nascondi' }} ChildComponent
    </button>
  `
})
export class ParentComponent implements OnInit {
  greeting = 'hello';
  hidden = false;

  constructor() {
    console.log('+ Parent component: constructor');
  }
  ngOnInit() {
    console.log('+ Parent component: ngOnInit');
  }
}
import { Component, OnInit, Input, OnDestroy } from '@angular/core';

@Component({
  selector: 'simple-child-component',
  template: `
    <p>
      Messaggio ricevuto dal padre: {{ message }}
    </p>
  `
})
export class ChildComponent implements OnInit, OnDestroy {

  @Input() message: string;

  constructor() {
    console.log('>> ChildComponent: constructor');
    console.log('>> ChildComponent: valore messaggio' +
      'ricevuto nel costruttore ->', this.message
    );
  }

  ngOnInit() {
    console.log('>> ChildComponent: ngOnInit');
    console.log('>> ChildComponent: valore messaggio' +
      'ricevuto in ngOnInit ->', this.message
    );
  }

  ngOnDestroy() {
    console.log('Destroying ChildComponent...');
    console.log('☠️️️⚠️ ChildComponent: ngOnDestroy ⚠️☠️');
  }

}

Il metodo ngOnChanges()

Il lifecycle hook ngOnChanges() viene invocato quando viene rilevata una modifica delle proprietà a cui viene applicato il decoratore @Input(). Detto in altri termini, se il componente di livello superiore passa delle nuove informazioni al componente figlio, viene invocato il metodo ngOnChanges di quest’ultimo. (Faremo una precisazione in merito a tale affermazione alla fine di questo paragrafo)

La segnatura del metodo è la seguente:

ngOnChanges(changes: SimpleChanges): void {}

Notiamo che il Lifecycle hook ngOnChanges() presenta un parametro changes di tipo SimpleChanges.

interface SimpleChanges {
  __index(propName: string): SimpleChange
}

Ogni volta che il metodo ngOnChanges() viene invocato, viene passato come argomento un oggetto che ha tante proprietà quanto quelle, a cui è stato applicato il decoratore @Input(), che sono state aggiornate. Tale oggetto avrà quindi delle coppie chiavi-valore, in cui la chiave è il nome di una proprietà @Input che ha subito una variazione e il valore è un oggetto di tipo SimpleChange.

SimpleChange è una classe con tre proprietà e un metodo:

class SimpleChange {
  constructor(previousValue: any, currentValue: any, firstChange: boolean)
  previousValue: any
  currentValue: any
  firstChange: boolean
  isFirstChange(): boolean
}
  • previousValue rappresenta il valore della proprietà di input prima dell’ultima modifica;
  • currentValue rappresenta il nuovo valore aggiornato della proprietà di input;
  • firstChange è un valore booleano che è pari a true se è la prima modifica subita da una proprietà di input;
  • isFirstChange() restituisce il valore di firstChange.

Vediamo quindi un semplice esempio in cui abbiamo un componente padre AppComponent e un componente figlio CarDetailsComponent come mostrato sotto.

import { CarDetailsComponent } from './car-details.component';
import { Component } from '@angular/core';

@Component({
  selector: 'simple-root',
  template: `
    <simple-car-details
      [model]="carModel"
      [year]="carYear">
    </simple-car-details>
    <button (click)="updateCar()">Aggiorna auto</button>
  `
})
export class AppComponent {
  carModel = 'Ferrari 250 GTO';
  carYear = 1962;

  updateCar() {
    this.carModel = 'Ferrari 288 GTO';
    this.carYear = 1984;
  }
}

Nel componente AppComponent abbiamo due proprietà che vengono passate al componente CarDetailsComponent. Attraverso un pulsante è possibile modificare il valore delle due proprietà e aggiornare l’applicazione per rispecchiare i nuovi valori.

import { Component, Input, SimpleChanges, OnChanges } from '@angular/core';

@Component({
  selector: 'simple-car-details',
  template: `
    <p>{{ model }} - {{ year }}</p>
    <pre>
      <code>
        {{ changes }}
      </code>
    </pre>
  `
})
export class CarDetailsComponent implements OnChanges {
  @Input() model;
  @Input() year;
  changes: string;

  ngOnChanges(changes: SimpleChanges): void {
    this.changes = JSON.stringify(changes, null, 2);
  }
}

Nel componente CarDetailsComponent viene invocato il Lifecycle Hook ngOnChanges() ogni volta che le proprietà di input vengono modificate.

Dopo aver cliccato il pulsante per la prima volta, l’oggetto changes è il seguente:

{
  "model": {
    "previousValue": "Ferrari 250 GTO",
    "currentValue": "Ferrari 288 GTO",
    "firstChange": false
  },
  "year": {
    "previousValue": 1962,
    "currentValue": 1984,
    "firstChange": false
  }
}

Se avessimo invece aggiornato solo la proprietà model, avremmo ottenuto un risultato simile a quello mostrato sotto.

In precedenza abbiamo affermato che il metodo ngOnChanges() viene invocato ogni volta che le proprietà di input di un componente vengono modificate. In realtà non è esattamente così. Infatti il metodo ngOnChanges() viene sempre eseguito solo se le proprietà di input sono di tipo primitivo (String, Number, ecc..) e vengono modificate. Se invece contengono un riferimento, ngOnChanges() viene invocato solo se cambia il riferimento stesso, ovvero se viene assegnato un nuovo oggetto o array. Se per esempio una proprietà di input contiene un riferimento a un oggetto e a cambiare è solamente una sua proprietà, il metodo ngOnchanges() non viene eseguito. Utilizzando la strategia standard di Change Detection di Angular, la view del componente viene comunque aggiornata, ma il metodo ngOnChanges() non viene invocato come possiamo vedere dall’esempio riporatato sotto in cui abbiamo modificato il codice visto in precedenza per illustrare il concetto appena esposto.

import { CarDetailsComponent } from './car-details.component';
import { Component } from '@angular/core';

@Component({
  selector: 'simple-root',
  template: `
    <simple-car-details
      [car]="car">
    </simple-car-details>
    <button (click)="updateCar()">Aggiorna auto</button>
    <button (click)="updateCarYear()">Crea nuova auto</button>
  `
})
export class AppComponent {
  car = {
    model: 'Ferrari 250 GTO',
    year: 1962
  };

  updateCar() {
    this.car.model = 'Ferrari 288 GTO';
    this.car.year = 1984;
  }

  updateCarYear() {
    // aggiorna la proprietà 'year',
    // ma assegna un riferimento ad
    // un nuovo oggetto a 'this.car'
    // In alternativa è possibile usare Object.assign()
    this.car = { ...this.car, year: 1985 };
  }
}
import { Component, Input, SimpleChanges, OnChanges } from '@angular/core';

@Component({
  selector: 'simple-car-details',
  template: `
    <p>{{ car.model }} - {{ car.year }}</p>
    <pre>
      <code>
        {{ changes }}
      </code>
    </pre>
  `
})
export class CarDetailsComponent implements OnChanges {
  @Input() car;
  changes: string;

  ngOnChanges(changes: SimpleChanges): void {
    console.log('ngOnchanges');
    this.changes = JSON.stringify(changes, null, 2);
  }
}

Il metodo ngDoCheck()

Come abbiamo visto il metodo ngOnChanges() non rileva tutte le modifiche apportate alle proprietà di input. Al contrario ngDoCheck() viene sempre invocato quando viene effettuata una modifica a un componente. La prima volta viene eseguito subito dopo ngOnInit(). Questo Lifecycle hook permette di effettuare delle operazioni tutte le volte che il metodo ngOnChanges() non riesce a rilevare le modifiche apportate alle proprietà di input.

Il metodo ngAfterContentInit()

Questo metodo viene invocato una volta sola in fase di inizializzazione di un componente dopo il metodo ngDoCheck(). In particolare, abbiamo visto che è possibile creare, grazie a <ng-content>, delle slot in cui vanno a finire gli elementi che vengono inseriti fra i tag dell’elemento associato a un componente. Il metodo ngAfterContentInit() viene chiamato dopo che è stata completata l’inizializzazione degli elementi passati al componente dall’esterno a cui si fa riferimento col nome Content Children.

Il metodo ngAfterContentChecked()

Il lifecycle hook ngAfterContentChecked() di un componente viene invocato dopo che il meccanismo di Change Detection di Angular ha controllato tutti i suoi Content Children. In fase di costruzione del componente, viene chiamato dopo il metodo ngAfterContentInit(). Nei controlli successivi viene invece eseguito dopo il metodo ngDoCheck().

Il metodo ngAfterViewInit()

Il metodo ngAfterViewInit() viene invocato immediatamente dopo che Angular ha completato l’inizializzazione della view di un componente e dei suoi View Children. Il metodo che lo precede è ngAfterContentChecked().

Il metodo ngAfterViewChecked()

Simile a ngAfterContentChecked(), ma invocato in questo caso la prima volta dopo il metodo ngAfterViewInit(). Successivamente viene preceduto dal Lifecycle hook ngAfterContentChecked() e viene eseguito non appena Angular ha controllato la view del componente e dei suoi View Children.

Prima di concludere questa lezione riportiamo di seguito un’immagine che illustra in che ordine vengono eseguiti i Lifecycle hooks nel caso dei due semplici componenti padre e figlio dell’esempio visto all’inizio di questa lezione quando abbiamo parlato del metodo ngOnDestroy.

esempio ordine esecuzione metodi lifecycle componenti angular

Riepilogo

In questa lezione abbiamo illustrato quali sono i cosiddetti Lifecycle hooks attraverso i quali possiamo intercettare le diverse fasi del ciclo di vita di un componente. Nella prossima lezione torneremo ad affrontare un altro argomento relativo ai template dei componenti e in particolare parleremo di Template reference variables.

Pubblicitร