Angular: Hooks del ciclo de vida de un componente

Antes de empezar a describir los Hooks del ciclo de vida de un componente quiero citar la definición de Hooking de wikipedia:
El termino Hookingabarca una gama de técnicas utilizadas para alterar o aumentar el comportamiento de un sistema operativo, aplicaciones o de otros componentes de software interceptando llamadas de función o mensajes o eventos pasados entre componentes de software. El código que maneja tales llamadas de función, eventos o mensajes interceptados se le llama un Hook.

En angular tenemos estos hooks, que son funciones ejecutadas en puntos claves de la vida de un componente.
Tanto los componentes y las directivas pueden ejecutar estas funciones, pero las directivas no ejecutan las funciones de color azul, las cuales son especificas de la vista y contenido de componentes.
Dado que normalmente se utiliza typescript en Angular para obtener el código javascript final, vamos a implementar estos métodos utilizando la interfaz que le corresponde a cada uno. Todas estas interfaces están en el modulo @angular/core.

Por ejemplo, para implementar el método ngOnInit, debemos importar la interfaz OnInit:
import { Component, OnInit } from '@angular/core';
El lenguaje de javascript no tiene interfaces, estas interfaces las agrega typescript para obtener un Javascript fuertemente tipeado y basado en clases. Aunque las ultimas versiones de javascript utilizan una sintaxis de clases casi idéntica, por detrás realmente son funciones y herencia prototipal.
Más abajo tenemos ejemplos de todos los hooks en acción y están basados en el código de la documentación oficial de Angular.
Existe un componente de tipo ParentComponenty otro ChildComponent. El ParentComponentse encarga de crear el ChildComponentpara poder observar el constructor, OnInit
y todos los demás hooks. También se encarga de bindear la propedad name
y actualizarla para poder ver en acción el hook ngOnChanges
.
Como te darás cuenta viendo del archivo /app/all/child.component.ts
, este implementa todas las interfaces de los hooks. Para ver el código fuente, presiona el botón.
Si presionas el botón Crear ChildComponent, el mensaje #1es en el constructor
del componente, el constructor normalmente se utiliza para definir valores iniciales simples, nunca pongas mucha lógica en el constructor. Además podemos notar que la propiedad name
en este punto, aun no esta definida.
Después tenemos el mensaje #2, este es del hook ngOnChanges
, aquí es cuando ocurre el enlazado de las propiedades con la nueva instancia de nuestro ChildComponent, en este punto la propiedad name
ahora si esta definida.
Luego tenemos la ejecución de ngOnInit, ngDoCheck, ngAfterContentInit, ngAfterContentChecked, ngAfterViewInit, ngAfterViewCheckeden ese orden.
Ahora si presionas el botón Actualizar ChildComponent, se actualiza la propiedad name
desde el ParentComponent, podemos ver como se ejecutan ngDoCheck
, ngAfterContentChecked
, ngAfterViewChecked
, y otra vez ngOnChanges
, ngDoCheck
, ngAfterContentChecked
y finalmente ngAfterViewChecked
. Aquí podemos afirmar dos cosas:
ngDoCheck
,ngAfterContentChecked
,ngAfterViewChecked
se invocan en ese orden al detectar un cambio.- También se ejecutan siempre tras un
ngOnChanges
pues se volvieron a ejecutar en ese mismo orden
Finalmente, si presionas el botón Destruir ChildComponent, los hooks ngDoCheck
, ngAfterContentChecked
, ngAfterViewChecked
se vuelven a ejecutar, y además antes de destruir el componente totalmente, se ejecuta el hook ngOnDestroy
.
Contenido
ngOnChanges
Cuando se crea un componente lo primero que se ejecuta es el constructor de la clase del componente, pero esto es normal, pues es la creación una nueva instancia de una clase, así que realmente constructor no es un hook de angular.
ngOnChanges
solo revisa los cambios de una propiedad input, esto es, todas las propiedades a las que se les aplica el decorador @input([alias])
, que es la manera en que bindeamos o enlazamos propiedades de un componente padre a otro hijo usando esta sintaxis [habilidad]="expresion"
. Este patrón, de padre a hijo, nos permite obtener un mejor rendimiento en la detección de cambios entre componentes. Es por eso que en el código de los ejemplos siempre se utiliza un componente padre y un componente hijo, los cambios se actualizan de padre a hijo.
En el ejemplo tenemos dos propiedades enlazadas [persona]="persona"
y [habilidad]="habilidad"
, si cambiamos el valor de habilidad se dispara la ejecución de ngOnChanges
en el componente hijo, sin embargo la propiedad persona.name
no dispara el hook, esto pasa porque la detección de cambios comprueba la referencia de la propiedad persona y lo que se modifico fue la propiedad persona.name
, por lo que la referencia al objeto persona nunca cambió.
Cada vez que se detecta un cambio, ngOnChanges
recibe un objeto, el cual contiene todas las propiedades que cambiaron, y cada una de estas es de tipo SimpleChangeconteniendo el valor anterior previousValue
y el valor actual currentValue
.
ngOnChanges
puede detectar cambios en más de una propiedad @Input()
, por lo que es muy útil para realizar múltiples operaciones en la información, como definir valores predeterminados, validar datos, transformarlos antes de ser utilizados por el componente y/o antes de ser visualizados por el usuario.
ngOnInit y ngOnDestroy
ngOnInit
El hook ngOnInit
se ejecuta una sola vez después de ngOnChanges
y no se vuelve a ejecutar en el resto de vida del componente. Como ya habíamos comentado antes, una directiva puede tener hooks, esto se debe a que un componente es un tipo de directiva que tiene una plantilla html. Es decir un componente es un tipo de directiva.
Dado que ngOnInit
se ejecuta una sola vez después de ngOnChanges, es un buen lugar para:
- Agregar inicializaciones complejas, como algún proceso asíncrono, obtener de un servicio datos del componente, etc.
- Configuración de inicio debido a que ya se tienen los valores de las propiedades
@inputs()
gracias al hookngOnChanges
.
Cada vez que se presiona el botón Agregar Personael hook ngOnInit
se ejecuta, y cuando presionas el botón Resetearse ejecuta el hook ngOnDestroy
justo antes de destruir a las elementos personas.
ngOnDestroy
En este hook debería de ir todo el código de limpieza de un componente, justo antes de destruirlo.
También es un buen momento para notificar a otros componentes que va a ser destruido.
Es un buen punto donde liberar memoria, darse de baja de los Observadores como RxJS y los eventos del DOM. Detener los timers, dar de baja todos los callbacks enlazados a servicios. En general todo lo que consuma memoria y tiempo de ejecución.
ngDoCheck
Este Hook se ejecuta después de cualquier ejecución de ngOnChange, y despues de ngOnInit, pero también se ejecuta después de eventos que pueden producir cambios, como por ejemplo el blur y focus de los inputs, por lo que se ejecuta demasiadas veces, es muy costoso utilizar este hook, así que se debe usar con cuidado.
Para crear este ejemplo simplemente se implementó el método ngDoCheck
en el componente hijo del código del hook ngOnChanges
. Puedes probar cambiando datos y veras la ejecuciones de la función ngDoCheck
.
ngOnChanges
NO se ejecuta cuando modificamos una propiedad que no es un @input()
, como pasa con la propiedad name
del objeto persona
, pero si se ejecuta el hook ngDoCheck
. Puede ser util para detectar cambios internos del componente, pero recuerda que tiene un costo alto de ejecución si deseas detectar muchas propiedades NO @inputs
de tu componente.
También tenemos un elemento input dentro del componente hijo que esta enlazado a la propiedad [habilidad]
, esta es un @input
, pero el cambio se realiza dentro del componente hijo, el componente padre no se entera de este cambio, por lo que aunque ngDoCheck
se ejecuta, ngOnChanges
no lo hace. Recuerda, la detección de cambios entre componentes se realiza de manera unidireccional, de padre a hijo.
AfterContentInit y AfterContentChecked
ngAfterContentInit
se ejecuta una vez después del primer ngDoCheck
y no se vuelve a ejecutar en todo el resto de vida del componente. ngAfterContentChecked
se ejecuta después de cualquier ejecución de ngDoCheck
en todo la vida del componente. Ambos se ejecutan después de que angular proyecta contenido externo en el componente, este contenido externo es proporcionado por el usuario del componente, similar a lo que se hace con el Light DOMde los componentes web nativos.
A lo anterior Angular le llama Proyección de contenido(Content projection), en AngularJS se conoce como transclusion. Para proyectar el contenido en el componente, se debe utilizar el tag <ng-content></ng-content>
, esta etiqueta sería analoga a la etiqueta <slot>
de los componenes web nativos.
Dentro del método ngAfterContentChecked
se debe poder acceder al modelo o variables del contenido proyectado, para esto se debe utilizar el decorador @ContentChild([TipoDeHijo])
, lo puedes ver en el archivo /app/after-content/after-content-child.component.ts
.
// Para buscar contenido de tipo 'ChildComponent'(etiqueta lch-child)
@ContentChild(ChildComponent) contentChild: ChildComponent;
AfterViewInit y AfterViewChecked
Estos hooks son muy similares a ngAfterContentInit
y ngAfterContentCheck
, solo que se invocan después de ngAfterContentChecked
, siguiendo el mismo patron, ngAfterContentInit
se invoca una sola vez después del primer ngAfterContentChecked
, y no se vuelve a invocar.
ngAfterViewChecked
, se ejecuta después de cualquier invocación de ngAfterContentChecked
, la diferencia entre ngAfterContentChecked
es que estos hooks marcan el final de la composición de la vista de un componente y se define dentro del componente mismo, no desde el exterior, como vimos con ngAfterContentChecked
y ng-content
, por esta razón tiene similitud con el Shadow DOMde los componentes web nativos.
Si revisas el código del /app/after-view/after-view-child.component.ts
, para acceder al modelo o variables de la vista interna (Vista de hijos o Child View), debemos utilizar el decorator @ViewChild([TipoDeHijo])
:
// Para buscar view childs del tipo ChildComponent
@ViewChild(ChildComponent) viewChild: ChildComponent;
Otra cosa distinta a ngAfterContentChecked
es que en ngAfterViewChecked
no podemos cambiar el valor de inmediato de la propiedad comment
enlazada con la vista, si lo haces, te saldrá un error como el siguiente:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: ''. Current value: 'Es un nombre muy grande'.
El error se debe que esté ultimo hook se ejecuta al final del ciclo de vida, entonces debe volver a pasar por ngOnChanges
o ngDocheck
para volver a renderizar la vista actualizada. La solución es actualizar la propiedad comment
en una futura iteración del loop de Javascript. El setTimeout de this.logger.thick_then
recibe una función, la cual se agrega a la cola de callbacks de javascript para ser escogida en una futura iteración del loop y esto le da el tiempo suficiente a Angular para actualizar la vista correctamente.
En un futuro post explicare el funcionamiento del Loop de Javascript, el cual es casi idéntico al Loop de node.js, creo que es un punto importante que vale la pena explicar.
Muchas gracias por el post!!!