Artigo original: https://www.freecodecamp.org/news/angular-lifecycle-hooks/

Por que precisamos de hooks do ciclo de vida?

Os frameworks modernos de front-end movem a aplicação de um estado para outro. Os dados alimentam essas atualizações. Essas tecnologias interagem com os dados que, por sua vez, fazem a transição do estado. A cada mudança de estado, há muitos momentos específicos em que determinados ativos ficam disponíveis.

Em um momento, o modelo pode estar pronto. Em outro, os dados terão terminado de ser carregados. A programação de cada exemplo requer um meio de detecção. Os hooks de ciclo de vida atendem a essa necessidade. Os frameworks modernos de front-end vêm com uma variedade de hooks de ciclo de vida. O Angular não é uma exceção.

Hooks de ciclo de vida explicados

Os hooks de ciclo de vida são métodos programados para serem executados em determinado momento. Eles diferem em quando e por que são executados. A detecção de alterações aciona esses métodos. Eles são executados de acordo com as condições do ciclo atual. O Angular executa a detecção de alterações constantemente em seus dados. Os hooks de ciclo de vida ajudam a gerenciar seus efeitos.

Um aspecto importante desses hooks é sua ordem de execução. Ela nunca muda. Eles são executados com base em uma série previsível de eventos de carregamento produzidos a partir de um ciclo de detecção. Isso os torna previsíveis.

Alguns ativos só estão disponíveis após a execução de um determinado hook. Obviamente, um hook só é executado sob determinadas condições definidas no ciclo de detecção de alterações atual.

Este artigo apresenta os hooks de ciclo de vida na ordem de sua execução (se todos forem executados). Certas condições merecem a ativação de um hook. Há alguns que são executados apenas uma vez, após a inicialização do componente.

Todos os métodos do ciclo de vida estão disponíveis em @angular/core. Embora não seja obrigatório, o Angular recomenda a implementação de todos os hooks (texto em inglês). Essa prática resulta em mensagens de erro mais relacionadas ao componente.

Ordem de execução dos hooks de ciclo de vida

ngOnChanges

O ngOnChanges é acionado após a modificação dos membros da classe vinculada @Input. Os dados vinculados pelo decorador @Input() são provenientes de uma fonte externa. Quando a fonte externa altera esses dados de maneira detectável, eles passam novamente pela propriedade @Input.

Com essa atualização, o ngOnChanges é acionado imediatamente. Ele também é acionado na inicialização dos dados de entrada. O hook recebe um parâmetro opcional do tipo SimpleChanges. Esse valor contém informações sobre as propriedades de limite de entrada alteradas.

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

@Component({
  selector: 'app-child',
  template: `
  <h3>Child Component</h3>
  <p>TICKS: {{ lifecycleTicks }}</p>
  <p>DATA: {{ data }}</p>
  `
})
export class ChildComponent implements OnChanges {
  @Input() data: string;
  lifecycleTicks: number = 0;

  ngOnChanges() {
    this.lifecycleTicks++;
  }
}

@Component({
  selector: 'app-parent',
  template: `
  <h1>ngOnChanges Example</h1>
  <app-child [data]="arbitraryData"></app-child>
  `
})
export class ParentComponent {
  arbitraryData: string = 'initial';

  constructor() {
    setTimeout(() => {
      this.arbitraryData = 'final';
    }, 5000);
  }
}

Resumo: o ParentComponent (componente pai) vincula os dados de entrada ao ChildComponent (componente filho). O componente recebe esses dados por meio de sua propriedade @Input e ngOnChanges é acionado. Após cinco segundos, a chamada de retorno setTimeout é acionada. O ParentComponent altera a fonte de dados da propriedade do limite de entrada do ChildComponent. Os novos dados fluem pela propriedade de entrada. O ngOnChanges é acionado mais uma vez.

ngOnInit

O ngOnInit é acionado uma vez na inicialização das propriedades vinculadas à entrada (@Input) de um componente. O próximo exemplo será semelhante ao anterior. O hook não é acionado quando o ChildComponent recebe os dados de entrada. Em vez disso, ele é acionado logo após os dados serem renderizados no modelo ChildComponent.

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

@Component({
  selector: 'app-child',
  template: `
  <h3>Child Component</h3>
  <p>TICKS: {{ lifecycleTicks }}</p>
  <p>DATA: {{ data }}</p>
  `
})
export class ChildComponent implements OnInit {
  @Input() data: string;
  lifecycleTicks: number = 0;

  ngOnInit() {
    this.lifecycleTicks++;
  }
}

@Component({
  selector: 'app-parent',
  template: `
  <h1>ngOnInit Example</h1>
  <app-child [data]="arbitraryData"></app-child>
  `
})
export class ParentComponent {
  arbitraryData: string = 'initial';

  constructor() {
    setTimeout(() => {
      this.arbitraryData = 'final';
    }, 5000);
  }
}

Resumo: o ParentComponent vincula os dados de entrada ao ChildComponent. O ChildComponent recebe esses dados por meio de sua propriedade @Input. Os dados são renderizados para o modelo e o ngOnInit é acionado. Após cinco segundos, a chamada de retorno setTimeout é acionada. O ParentComponent muda a fonte de dados da propriedade vinculada à entrada de ChildComponent e ngOnInit NÃO É ACIONADO.

ngOnInit é um hook de uso único. A inicialização é sua única preocupação.

ngDoCheck

O ngDoCheck é acionado a cada ciclo de detecção de alterações. O Angular executa a detecção de alterações com frequência. O desempenho de qualquer ação fará com que ele entre em ciclo e ngDoCheck é acionado com esses ciclos. Use-o com cautela. Ele pode criar problemas de desempenho quando implementado incorretamente.

ngDoCheck permite que os desenvolvedores verifiquem seus dados manualmente. Eles podem acionar uma nova data de aplicação condicionalmente. Em conjunto com ChangeDetectorRef, os desenvolvedores podem criar suas próprias verificações para detecção de alterações.

import { Component, DoCheck, ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'app-example',
  template: `
  <h1>ngDoCheck Example</h1>
  <p>DATA: {{ data[data.length - 1] }}</p>
  `
})
export class ExampleComponent implements DoCheck {
  lifecycleTicks: number = 0;
  oldTheData: string;
  data: string[] = ['initial'];

  constructor(private changeDetector: ChangeDetectorRef) {
    this.changeDetector.detach(); // lets the class perform its own change detection

    setTimeout(() => {
      this.oldTheData = 'final'; // intentional error
      this.data.push('intermediate');
    }, 3000);

    setTimeout(() => {
      this.data.push('final');
      this.changeDetector.markForCheck();
    }, 6000);
  }

  ngDoCheck() {
    console.log(++this.lifecycleTicks);

    if (this.data[this.data.length - 1] !== this.oldTheData) {
      this.changeDetector.detectChanges();
    }
  }
}

Preste atenção no console e contrate-o com o que aparece na tela. Os dados progridem até "intermediário" antes de congelar. Três rodadas de detecção de mudanças ocorrem durante esse período, conforme indicado no console. Mais uma rodada de detecção de alterações ocorre quando 'final' é empurrado para o final de this.data. Em seguida, ocorre uma última rodada de detecção de mudanças. A avaliação da instrução if determina que não são necessárias atualizações na exibição.

Resumo: a classe é exemplificada após duas rodadas de detecção de alterações. O construtor da classe inicia o setTimeout duas vezes. Após três segundos, o primeiro setTimeout aciona a detecção de alterações e o ngDoCheck marca a tela para uma atualização. Três segundos depois, o segundo setTimeout aciona a detecção de alterações. Não são necessárias atualizações de visualização de acordo com a avaliação de ngDoCheck.

Aviso

Antes de continuar, aprenda a diferença entre o DOM de conteúdo e o DOM de visualização (DOM significa Modelo de Objeto do Documento).

O DOM de conteúdo define o innerHTML dos elementos da diretiva. Por outro lado, o DOM de visualização é o modelo de um componente, excluindo qualquer modelo HTML agrupado em uma diretiva. Para entender melhor, consulte este artigo do blog (texto em inglês) do autor Minko Gechev.

ngAfterContentInit

ngAfterContentInit é acionado depois que o DOM do conteúdo do componente é inicializado (carregado pela primeira vez). Esperar por pesquisas (do inglês, queries) de @ContentChild(ren) é o principal caso de uso do hook.

As pesquisas de @ContentChild(ren) produzem referências de elementos para o DOM de conteúdo. Desse modo, eles não ficam disponíveis até que o DOM do conteúdo seja carregado. Por isso, o ngAfterContentInit e seu equivalente ngAfterContentChecked são usados.

import { Component, ContentChild, AfterContentInit, ElementRef, Renderer2 } from '@angular/core';

@Component({
  selector: 'app-c',
  template: `
  <p>I am C.</p>
  <p>Hello World!</p>
  `
})
export class CComponent { }

@Component({
  selector: 'app-b',
  template: `
  <p>I am B.</p>
  <ng-content></ng-content>
  `
})
export class BComponent implements AfterContentInit {
  @ContentChild("BHeader", { read: ElementRef }) hRef: ElementRef;
  @ContentChild(CComponent, { read: ElementRef }) cRef: ElementRef;

  constructor(private renderer: Renderer2) { }

  ngAfterContentInit() {
    this.renderer.setStyle(this.hRef.nativeElement, 'background-color', 'yellow')

    this.renderer.setStyle(this.cRef.nativeElement.children.item(0), 'background-color', 'pink');
    this.renderer.setStyle(this.cRef.nativeElement.children.item(1), 'background-color', 'red');
  }
}

@Component({
  selector: 'app-a',
  template: `
  <h1>ngAfterContentInit Example</h1>
  <p>I am A.</p>
  <app-b>
    <h3 #BHeader>BComponent Content DOM</h3>
    <app-c></app-c>
  </app-b>
  `
})
export class AComponent { }

Os resultados da pesquisa de @ContentChild estão disponíveis em ngAfterContentInit. O Renderer2 atualiza o DOM de conteúdo do BComponent, que contém uma tag h3 e o CComponent. Esse é um exemplo comum de projeção de conteúdo (texto em inglês).

Resumo: a renderização começa com o AComponent. Para concluir, o AComponent deve renderizar o BComponent. O BComponent projeta o conteúdo agrupado em seu elemento por meio do elemento <ng-content></ng-content>. O CComponent faz parte do conteúdo projetado. O conteúdo projetado termina de ser renderizado. O ngAfterContentInit é acionado. O BComponent termina a renderização. O AComponent termina a renderização. ngAfterContentInit não será acionado novamente.

ngAfterContentChecked

O ngAfterContentChecked é acionado após cada ciclo de detecção de alterações direcionado ao DOM de conteúdo. Isso permite que os desenvolvedores facilitem o modo como o DOM do conteúdo reage à detecção de alterações. O ngAfterContentChecked pode ser acionado com frequência e causar problemas de desempenho se for implementado incorretamente.

O ngAfterContentChecked também é acionado durante os estágios de inicialização de um componente. Ele vem logo após ngAfterContentInit.

import { Component, ContentChild, AfterContentChecked, ElementRef, Renderer2 } from '@angular/core';

@Component({
  selector: 'app-c',
  template: `
  <p>I am C.</p>
  <p>Hello World!</p>
  `
})
export class CComponent { }

@Component({
  selector: 'app-b',
  template: `
  <p>I am B.</p>
  <button (click)="$event">CLICK</button>
  <ng-content></ng-content>
  `
})
export class BComponent implements AfterContentChecked {
  @ContentChild("BHeader", { read: ElementRef }) hRef: ElementRef;
  @ContentChild(CComponent, { read: ElementRef }) cRef: ElementRef;

  constructor(private renderer: Renderer2) { }

  randomRGB(): string {
    return `rgb(${Math.floor(Math.random() * 256)},
    ${Math.floor(Math.random() * 256)},
    ${Math.floor(Math.random() * 256)})`;
  }

  ngAfterContentChecked() {
    this.renderer.setStyle(this.hRef.nativeElement, 'background-color', this.randomRGB());
    this.renderer.setStyle(this.cRef.nativeElement.children.item(0), 'background-color', this.randomRGB());
    this.renderer.setStyle(this.cRef.nativeElement.children.item(1), 'background-color', this.randomRGB());
  }
}

@Component({
  selector: 'app-a',
  template: `
  <h1>ngAfterContentChecked Example</h1>
  <p>I am A.</p>
  <app-b>
    <h3 #BHeader>BComponent Content DOM</h3>
    <app-c></app-c>
  </app-b>
  `
})
export class AComponent { }

Ele não difere muito do ngAfterContentInit. Um mero <button></button> foi adicionado ao BComponent. Clicar nele causa um loop de detecção de alterações. Isso ativa o gancho, conforme indicado pela aleatoriedade da cor de fundo.

Resumo: a renderização começa com o AComponent. Para concluir, o AComponent deve renderizar o BComponent. O BComponent projeta o conteúdo agrupado em seu elemento por meio do elemento <ng-content></ng-content>. O CComponent faz parte do conteúdo projetado. O conteúdo projetado termina de ser renderizado. ngAfterContentChecked é acionado. O BComponent termina a renderização. AComponent termina a renderização. O ngAfterContentChecked pode ser acionado novamente por meio da detecção de alterações.

ngAfterViewInit

O ngAfterViewInit é acionado uma vez após a conclusão da inicialização do DOM de visualização. A visualização sempre é carregada logo após o conteúdo. ngAfterViewInit aguarda a resolução das pesquisas @ViewChild(ren). Esses elementos são consultados na mesma visualização do componente.

No exemplo abaixo, o título h3 de BComponent é pesquisado. ngAfterViewInit é executado assim que os resultados da pesquisa estão disponíveis.

import { Component, ViewChild, AfterViewInit, ElementRef, Renderer2 } from '@angular/core';

@Component({
  selector: 'app-c',
  template: `
  <p>I am C.</p>
  <p>Hello World!</p>
  `
})
export class CComponent { }

@Component({
  selector: 'app-b',
  template: `
  <p #BStatement>I am B.</p>
  <ng-content></ng-content>
  `
})
export class BComponent implements AfterViewInit {
  @ViewChild("BStatement", { read: ElementRef }) pStmt: ElementRef;

  constructor(private renderer: Renderer2) { }

  ngAfterViewInit() {
    this.renderer.setStyle(this.pStmt.nativeElement, 'background-color', 'yellow');
  }
}

@Component({
  selector: 'app-a',
  template: `
  <h1>ngAfterViewInit Example</h1>
  <p>I am A.</p>
  <app-b>
    <h3>BComponent Content DOM</h3>
    <app-c></app-c>
  </app-b>
  `
})
export class AComponent { }

Renderer2 altera a cor de fundo do título de BComponent. Isso indica que o elemento de visualização foi pesquisado com sucesso graças ao ngAfterViewInit.

Resumo: a renderização começa com o AComponent. Para concluir, o AComponent deve renderizar o BComponent. O BComponent projeta o conteúdo agrupado em seu elemento por meio do elemento <ng-content></ng-content>. CComponent faz parte do conteúdo projetado. O BComponent termina a renderização. O ngAfterViewInit é acionado. AComponent termina a renderização. ngAfterViewInit não será acionado novamente.

ngAfterViewChecked

O ngAfterViewChecked é acionado após qualquer ciclo de detecção de alterações que tenha como alvo a visualização do componente. O hook ngAfterViewChecked permite que os desenvolvedores facilitem o modo como a detecção de alterações afeta o DOM de visualização.

import { Component, ViewChild, AfterViewChecked, ElementRef, Renderer2 } from '@angular/core';

@Component({
  selector: 'app-c',
  template: `
  <p>I am C.</p>
  <p>Hello World!</p>
  `
})
export class CComponent { }

@Component({
  selector: 'app-b',
  template: `
  <p #BStatement>I am B.</p>
  <button (click)="$event">CLICK</button>
  <ng-content></ng-content>
  `
})
export class BComponent implements AfterViewChecked {
  @ViewChild("BStatement", { read: ElementRef }) pStmt: ElementRef;

  constructor(private renderer: Renderer2) { }

  randomRGB(): string {
    return `rgb(${Math.floor(Math.random() * 256)},
    ${Math.floor(Math.random() * 256)},
    ${Math.floor(Math.random() * 256)})`;
  }

  ngAfterViewChecked() {
    this.renderer.setStyle(this.pStmt.nativeElement, 'background-color', this.randomRGB());
  }
}

@Component({
  selector: 'app-a',
  template: `
  <h1>ngAfterViewChecked Example</h1>
  <p>I am A.</p>
  <app-b>
    <h3>BComponent Content DOM</h3>
    <app-c></app-c>
  </app-b>
  `
})
export class AComponent { }

Resumo: a renderização começa com o AComponent. Para concluir, o AComponent deve renderizar o BComponent. O BComponent projeta o conteúdo agrupado em seu elemento por meio do elemento <ng-content></ng-content>. CComponent faz parte do conteúdo projetado. O conteúdo projetado termina de ser renderizado. O BComponent termina a renderização. ngAfterViewChecked é acionado. O AComponent termina a renderização. O ngAfterViewChecked pode ser acionado novamente por meio da detecção de alterações.

Clicar no elemento <button></button> inicia uma rodada de detecção de alterações. O ngAfterContentChecked dispara e randomiza a cor de fundo dos elementos consultados a cada clique no botão.

ngOnDestroy

ngOnDestroy é acionado quando um componente é removido da visualização e do DOM subsequente. Esse hook oferece uma chance de reparar quaisquer pontas soltas antes da exclusão de um componente.

import { Directive, Component, OnDestroy } from '@angular/core';

@Directive({
  selector: '[appDestroyListener]'
})
export class DestroyListenerDirective implements OnDestroy {
  ngOnDestroy() {
    console.log("Goodbye World!");
  }
}

@Component({
  selector: 'app-example',
  template: `
  <h1>ngOnDestroy Example</h1>
  <button (click)="toggleDestroy()">TOGGLE DESTROY</button>
  <p appDestroyListener *ngIf="destroy">I can be destroyed!</p>
  `
})
export class ExampleComponent {
  destroy: boolean = true;

  toggleDestroy() {
    this.destroy = !this.destroy;
  }
}

Resumo: o botão é clicado. O membro destroy de ExampleComponent alterna para false. A instrução estrutural *ngIf é avaliada como falsa. ngOnDestroy é acionado. O *ngIf remove o <p></p> que servia como seu host. Esse processo se repete qualquer número de vezes que você clicar no botão para alternar destroy para false.

Conclusão

Lembre-se de que determinadas condições devem ser atendidas para cada hook. Eles sempre serão executados em ordem sequencial, independentemente. Isso torna os hooks previsíveis o suficiente para serem usados, mesmo que alguns não sejam executados.

Com os hooks de ciclo de vida, é fácil prever o momento de execução de uma classe. Eles permitem que os desenvolvedores acompanhem onde a detecção de alterações está ocorrendo e como a aplicação deve reagir. Eles ficam parados em códigos que exigem dependências baseadas em carregamentos disponíveis somente após algum tempo.

O ciclo de vida do componente caracteriza os frameworks modernos de front-end. O Angular organiza seu ciclo de vida fornecendo os hooks mencionados acima.

Recursos (em inglês)

Outras fontes (em inglês)