Ganchos de ciclo de vida angular: ngOnChanges, ngOnInit y más

¿Por qué necesitamos ganchos de ciclo de vida?

Los marcos frontales modernos mueven la aplicación de un estado a otro. Los datos alimentan estas actualizaciones. Estas tecnologías interactúan con los datos que a su vez cambian el estado. Con cada cambio de estado, hay muchos momentos específicos en los que ciertos activos están disponibles.

En un caso, la plantilla podría estar lista, en otro, los datos se habrán terminado de cargar. La codificación para cada instancia requiere un medio de detección. Los ganchos del ciclo de vida responden a esta necesidad. Los frameworks front-end modernos se empaquetan con una variedad de enlaces de ciclo de vida. Angular no es una excepción

Explicación de los ganchos del ciclo de vida

Los enlaces de ciclo de vida son métodos cronometrados. Se diferencian en cuándo y por qué se ejecutan. La detección de cambios activa estos métodos. Se ejecutan en función de las condiciones del ciclo actual. Las ejecuciones angulares cambian la detección constantemente en sus datos. Los ganchos del ciclo de vida ayudan a gestionar sus efectos.

Un aspecto importante de estos ganchos es su orden de ejecución. Nunca se desvía. Se ejecutan en función de una serie predecible de eventos de carga producidos a partir de un ciclo de detección. Esto los hace predecibles.

Algunos activos solo están disponibles después de que se ejecuta un determinado gancho. Por supuesto, un gancho solo se ejecuta bajo ciertas condiciones establecidas en el ciclo de detección de cambio actual.

Este artículo presenta los ganchos del ciclo de vida en orden de ejecución (si todos se ejecutan). Ciertas condiciones merecen la activación de un gancho. Hay algunos que solo se ejecutan una vez después de la inicialización del componente.

Todos los métodos del ciclo de vida están disponibles en @angular/core. Aunque no es obligatorio, Angular recomienda implementar cada gancho. Esta práctica conduce a mejores mensajes de error con respecto al componente.

Orden de ejecución de los ganchos del ciclo de vida

ngOnChanges

ngOnChangesdesencadena tras la modificación de @Inputmiembros de clase enlazados. Los datos vinculados por el @Input()decorador provienen de una fuente externa. Cuando la fuente externa altera esos datos de manera detectable, vuelve a pasar por la @Inputpropiedad.

Con esta actualización, ngOnChangesse dispara inmediatamente. También se activa al inicializar los datos de entrada. El gancho recibe un parámetro opcional de tipo SimpleChanges. Este valor contiene información sobre las propiedades vinculadas a la entrada modificadas.

import { Component, Input, OnChanges } from '@angular/core'; @Component({ selector: 'app-child', template: ` 

Child Component

TICKS: {{ lifecycleTicks }}

DATA: {{ data }}

` }) export class ChildComponent implements OnChanges { @Input() data: string; lifecycleTicks: number = 0; ngOnChanges() { this.lifecycleTicks++; } } @Component({ selector: 'app-parent', template: `

ngOnChanges Example

` }) export class ParentComponent { arbitraryData: string = 'initial'; constructor() { setTimeout(() => { this.arbitraryData = 'final'; }, 5000); } }

Resumen: ParentComponent enlaza los datos de entrada al ChildComponent. El componente recibe estos datos a través de su@Inputpropiedad. ngOnChangesincendios. Después de cinco segundos, sesetTimeoutactivaladevolución de llamada. ParentComponent muta la fuente de datos de la propiedad de enlace de entrada de ChildComponent. Los nuevos datos fluyen a través de la propiedad de entrada. ngOnChangesdispara una vez más.

ngOnInit

ngOnInitse activa una vez tras la inicialización de las @Inputpropiedades de un componente vinculado a la entrada ( ). El siguiente ejemplo se verá similar al anterior. El gancho no se activa cuando ChildComponent recibe los datos de entrada. Más bien, se activa justo después de que los datos se procesan en la plantilla ChildComponent.

import { Component, Input, OnInit } from '@angular/core'; @Component({ selector: 'app-child', template: ` 

Child Component

TICKS: {{ lifecycleTicks }}

DATA: {{ data }}

` }) export class ChildComponent implements OnInit { @Input() data: string; lifecycleTicks: number = 0; ngOnInit() { this.lifecycleTicks++; } } @Component({ selector: 'app-parent', template: `

ngOnInit Example

` }) export class ParentComponent { arbitraryData: string = 'initial'; constructor() { setTimeout(() => { this.arbitraryData = 'final'; }, 5000); } }

Resumen: ParentComponent enlaza los datos de entrada al ChildComponent. ChildComponent recibe estos datos a través de su@Inputpropiedad. Los datos se procesan en la plantilla. ngOnInitincendios. Después de cinco segundos, sesetTimeoutactivaladevolución de llamada. ParentComponent muta la fuente de datos de la propiedad de enlace de entrada de ChildComponent. ngOnInit NO DISPARA.

ngOnInites un gancho de una sola vez. La inicialización es su única preocupación.

ngDoCheck

ngDoCheckse dispara con cada ciclo de detección de cambios. Los recorridos angulares cambian la detección con frecuencia. La realización de cualquier acción provocará un ciclo. ngDoCheckincendios con estos ciclos. Úselo con precaución. Puede crear problemas de rendimiento cuando se implementa incorrectamente.

ngDoCheckpermite a los desarrolladores comprobar sus datos manualmente. Pueden activar una nueva fecha de solicitud condicionalmente. Junto con ChangeDetectorRef, los desarrolladores pueden crear sus propias comprobaciones para la detección de cambios.

import { Component, DoCheck, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'app-example', template: ` 

ngDoCheck Example

DATA: {{ data[data.length - 1] }}

` }) 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 atención a la consola frente a la pantalla. Los datos progresan hasta 'intermedio' antes de congelarse. Se producen tres rondas de detección de cambios durante este período, como se indica en la consola. Se produce una ronda más de detección de cambios cuando "final" se lleva al final de this.data. Entonces se produce una última ronda de detección de cambios. La evaluación de la instrucción if determina que no es necesario realizar actualizaciones en la vista.

Summary: Class instantiates after two rounds of change detection. Class constructor initiates setTimeout twice. After three seconds, the first setTimeout triggers change detection. ngDoCheck marks the display for an update. Three seconds later, the second setTimeout triggers change detection. No view updates needed according to the evaluation of ngDoCheck.

Warning

Before proceeding, learn the difference between the content DOM and view DOM (DOM stands for Document Object Model).

The content DOM defines the innerHTML of directive elements. Conversely, the view DOM is a component’s template excluding any template HTML nested within a directive. For a better understanding, refer to this blog post.

ngAfterContentInit

ngAfterContentInit fires after the component’s content DOM initializes (loads for the first time). Waiting on @ContentChild(ren) queries is the hook’s primary use-case.

@ContentChild(ren) queries yield element references for the content DOM. As such, they are not available until after the content DOM loads. Hence why ngAfterContentInit and its counterpart ngAfterContentChecked are used.

import { Component, ContentChild, AfterContentInit, ElementRef, Renderer2 } from '@angular/core'; @Component({ selector: 'app-c', template: ` 

I am C.

Hello World!

` }) export class CComponent { } @Component({ selector: 'app-b', template: `

I am B.

` }) 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: `

ngAfterContentInit Example

I am A.

BComponent Content DOM

` }) export class AComponent { }

The @ContentChild query results are available from ngAfterContentInit. Renderer2 updates the content DOM of BComponent containing a h3 tag and CComponent. This is a common example of content projection.

Summary: Rendering starts with AComponent. For it to finish, AComponent must render BComponent. BComponent projects content nested in its element through the element. CComponent is part of the projected content. The projected content finishes rendering. ngAfterContentInit fires. BComponent finishes rendering. AComponent finishes rendering. ngAfterContentInit will not fire again.

ngAfterContentChecked

ngAfterContentChecked fires after every cycle of change detection targeting the content DOM. This lets developers facilitate how the content DOM reacts to change detection. ngAfterContentChecked can fire frequently and cause performance issues if poorly implemented.

ngAfterContentChecked fires during a component’s initialization stages too. It comes right after ngAfterContentInit.

import { Component, ContentChild, AfterContentChecked, ElementRef, Renderer2 } from '@angular/core'; @Component({ selector: 'app-c', template: ` 

I am C.

Hello World!

` }) export class CComponent { } @Component({ selector: 'app-b', template: `

I am B.

CLICK ` }) 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: `

ngAfterContentChecked Example

I am A.

BComponent Content DOM

` }) export class AComponent { }

This hardly differs from ngAfterContentInit. A mere was added to BComponent. Clicking it causes a change detection loop. This activates the hook as indicated by the randomization of background-color.

Summary: Rendering starts with AComponent. For it to finish, AComponent must render BComponent. BComponent projects content nested in its element through the element. CComponent is part of the projected content. The projected content finishes rendering. ngAfterContentChecked fires. BComponent finishes rendering. AComponent finishes rendering. ngAfterContentChecked may fire again through change detection.

ngAfterViewInit

ngAfterViewInit fires once after the view DOM finishes initializing. The view always loads right after the content. ngAfterViewInit waits on @ViewChild(ren) queries to resolve. These elements are queried from within the same view of the component.

In the example below, BComponent’s h3 header is queried. ngAfterViewInit executes as soon as the query’s results are available.

import { Component, ViewChild, AfterViewInit, ElementRef, Renderer2 } from '@angular/core'; @Component({ selector: 'app-c', template: ` 

I am C.

Hello World!

` }) export class CComponent { } @Component({ selector: 'app-b', template: `

I am B.

` }) 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: `

ngAfterViewInit Example

I am A.

BComponent Content DOM

` }) export class AComponent { }

Renderer2 changes the background color of BComponent’s header. This indicates the view element was successfully queried thanks to ngAfterViewInit.

Summary: Rendering starts with AComponent. For it to finish, AComponent must render BComponent. BComponent projects content nested in its element through the element. CComponent is part of the projected content. The projected content finishes rendering. BComponent finishes rendering. ngAfterViewInit fires. AComponent finishes rendering. ngAfterViewInit will not fire again.

ngAfterViewChecked

ngAfterViewChecked fires after any change detection cycle targeting the component’s view. The ngAfterViewChecked hook lets developers facilitate how change detection affects the view DOM.

import { Component, ViewChild, AfterViewChecked, ElementRef, Renderer2 } from '@angular/core'; @Component({ selector: 'app-c', template: ` 

I am C.

Hello World!

` }) export class CComponent { } @Component({ selector: 'app-b', template: `

I am B.

CLICK ` }) 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: `

ngAfterViewChecked Example

I am A.

BComponent Content DOM

` }) export class AComponent { }

Summary: Rendering starts with AComponent. For it to finish, AComponent must render BComponent. BComponent projects content nested in its element through the element. CComponent is part of the projected content. The projected content finishes rendering. BComponent finishes rendering. ngAfterViewChecked fires. AComponent finishes rendering. ngAfterViewChecked may fire again through change detection.

Clicking the element initiates a round of change detection. ngAfterContentChecked fires and randomizes the background-color of the queried elements each button click.

ngOnDestroy

ngOnDestroy fires upon a component’s removal from the view and subsequent DOM. This hook provides a chance to clean up any loose ends before a component’s deletion.

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: ` 

ngOnDestroy Example

TOGGLE DESTROY

I can be destroyed!

` }) export class ExampleComponent { destroy: boolean = true; toggleDestroy() { this.destroy = !this.destroy; } }

Summary: The button is clicked. ExampleComponent’s destroy member toggles false. The structural directive *ngIf evaluates to false. ngOnDestroy fires. *ngIf removes its host . This process repeats any number of times clicking the button to toggle destroy to false.

Conclusion

Remember that certain conditions must be met for each hook. They will always execute in order of each other regardless. This makes hooks predictable enough to work with even if some do not execute.

With lifecycle hooks, timing the execution of a class is easy. They let developers track where change detection is occurring and how the application should react. They stall for code that requires load-based dependencies available only after sometime.

The component lifecycle characterizes modern front end frameworks. Angular lays out its lifecycle by providing the aforementioned hooks.

Sources

  • Angular Team. “Lifecycle Hooks”. Google. Accessed 2 June 2018
  • Gechev, Minko. “ViewChildren and ContentChildren in Angular”. Accessed 2 June 2018

Resources

  • Angular Documentation
  • Angular GitHub Repository
  • Lifecycle Hooks in Depth