Parte 7: Lifecycle Hooks¶
Una aplicación pequeña para ver cuándo hace Angular cada cosa con un
componente: cuándo lo crea, cuándo lo revisa y cuándo lo destruye. Esos
momentos se llaman hooks del ciclo de vida (lifecycle hooks): métodos que
Angular llama por ti y donde puedes enganchar tu código. Es una aplicación
Angular 22 creada con ng new, con Tailwind CSS y daisyUI, igual que la
Parte 5: Pipes App. Tiene dos páginas (Home y
About) y un navbar para pasar de una a otra. La página de inicio muestra una
tabla con cada hook y su descripción, y además escribe en la consola del
navegador una línea de color por cada uno cuando se ejecuta.
Proyecto: 07-lifecycle-hooks/. Sitio publicado:
angular-lifecycle.reskyon.com. Ábrelo
con la consola del navegador a la vista. Para correrlo en local usa npm start
dentro de la carpeta (http://localhost:4200/).
Esta página va creciendo: cada módulo nuevo se agrega al final. Por ahora cubre
el orden de los hooks, de los effect y de los callbacks de render, cómo se
entera un componente de que cambió un input y la diferencia entre una propiedad
normal y una señal.
Qué vas a aprender:
- El orden de los hooks. Qué se ejecuta primero, qué se ejecuta una sola vez y qué se repite en cada revisión (Módulo LH1).
- Los inputs y
ngOnChanges. Qué recibe el hook, cuándo corre y qué valor tenían el input antes y después (Módulo LH2). - Propiedad normal frente a señal. Por qué sin
zone.jsun campo normal no refresca la pantalla y una señal sí, y qué hooks se repiten (Módulo LH3).
Cada módulo sigue el mismo orden: el problema que resuelve, el código del repositorio, cómo funciona por dentro, errores comunes y preguntas de entrevista con su respuesta.
Esta página es una parte de la guía de estudio; allí están el mapa general y la ruta recomendada. Conviene haber visto antes la Parte 6: Reactive Forms App.
Módulo LH1: El orden de los hooks del ciclo de vida¶
El problema¶
Cuando abres una página, Angular crea el componente, le pasa datos, dibuja su plantilla y la revisa una y otra vez mientras algo cambia. Si no sabes en qué momento ocurre cada paso, acabas pidiendo una cosa antes de que exista: leer un dato que todavía no llegó, o tocar un elemento que aún no está en pantalla.
Los hooks son los puntos de ese recorrido donde Angular te avisa. Cada uno es un método con un nombre fijo que escribes en la clase del componente.
El código del repositorio¶
Los textos viven en un solo lugar,
home.constants.ts,
y de ahí salen tanto la tabla de la página como las líneas de la consola:
export const NG_LIFECYCLE_HOOKS = {
ngOnInit: "Runs once, after Angular sets the component's inputs",
ngOnChanges: 'Runs every time an input property changes (and once before ngOnInit)',
ngDoCheck: 'Runs on every change detection pass',
// ... un texto por cada hook, de ngAfterContentInit a onCleanup
}
Para escribir en la consola con color,
log.utils.ts
tiene una clase pequeña:
export class ConsoleLogger {
static format = (name: string, message: string = '', color: string = '#bada55') => {
console.log(`%s %c%s`, name, `color: ${color}; font-weight: bold;`, message)
}
}
El nombre sale como texto normal y el mensaje en color (verde por defecto). El color va al final para poder omitirlo.
Una página no puede abrir las herramientas del desarrollador (el navegador no lo
permite), así que la app muestra el atajo en su lugar. Es un componente
reutilizable,
console-hint.component.ts,
sin inputs: se importa ConsoleHintComponent en la página y se escribe
<app-console-hint /> donde se quiera el aviso (está en Home y en About).
home-page.component.ts
implementa los hooks y cada uno llama a ConsoleLogger.format con su nombre y
su texto (aquí se omiten los comentarios y los campos de los ejemplos de los
módulos siguientes):
export class HomePageComponent
implements
OnInit,
OnChanges,
DoCheck,
AfterContentInit,
AfterContentChecked,
AfterViewInit,
AfterViewChecked,
OnDestroy
{
hooks = Object.entries(NG_LIFECYCLE_HOOKS)
constructor() {
ConsoleLogger.format('Constructor', 'Constructor called')
}
basicEffect = effect((onCleanup) => {
ConsoleLogger.format('effect', NG_LIFECYCLE_HOOKS.effect)
onCleanup(() => {
ConsoleLogger.format('onCleanup', NG_LIFECYCLE_HOOKS.onCleanup)
})
})
ngOnInit() {
ConsoleLogger.format('ngOnInit', NG_LIFECYCLE_HOOKS.ngOnInit)
}
// ... ngOnChanges, ngDoCheck, ngAfterContentInit, ngAfterContentChecked,
// ngAfterViewInit y ngAfterViewChecked, igual que ngOnInit
afterNextRenderEffect = afterNextRender(() => {
ConsoleLogger.format('afterNextRender', NG_LIFECYCLE_HOOKS.afterNextRender)
})
afterEveryRenderEffect = afterEveryRender(() => {
ConsoleLogger.format('afterEveryRender', NG_LIFECYCLE_HOOKS.afterEveryRender)
})
ngOnDestroy() {
ConsoleLogger.format('ngOnDestroy', NG_LIFECYCLE_HOOKS.ngOnDestroy)
}
}
La plantilla recorre hooks con @for y dibuja una fila por cada par
nombre y descripción, así que la tabla y la consola nunca se contradicen.
implements no hace falta para que Angular llame al método (lo busca por su
nombre), pero así el compilador avisa si falta o si lo escribes mal.
effect, afterNextRender y afterEveryRender no son interfaces: son
funciones que registran un callback y se llaman al crear el componente.
El orden¶
Abre la página de inicio y mira la consola. Verás solo los nombres, en este orden (a cada uno lo acompaña su frase):
Constructor
ngOnInit
ngDoCheck
effect
ngAfterContentInit
ngAfterContentChecked
ngAfterViewInit
ngAfterViewChecked
afterNextRender
afterEveryRender
Después, cada vez que algo cambia en la página, se repiten ngDoCheck,
ngAfterContentChecked, ngAfterViewChecked y afterEveryRender. Si no
cambia nada, no se repiten (la app no usa zone.js, así que Angular no revisa
por su cuenta). onCleanup y ngOnDestroy aparecen al salir de la página, y
onCleanup también antes de que el effect vuelva a ejecutarse.
El ngOnChanges de HomePageComponent no aparece, porque la página no tiene
inputs. Los ngOnChanges que sí salen en la consola son los del componente del
título, que sí recibe inputs: se explican en el
Módulo LH2.
| Hook | Cuándo se ejecuta | Cuántas veces |
|---|---|---|
constructor |
Angular crea la instancia, antes que cualquier hook | Una |
ngOnChanges |
Cambia un input(); también justo antes de ngOnInit |
Cada cambio |
ngOnInit |
Ya están asignados los inputs | Una |
ngDoCheck |
En cada revisión de cambios | Muchas |
effect |
Al empezar y cada vez que cambia una señal que lee; ocurre durante la detección de cambios | Una y cada cambio |
ngAfterContentInit |
Quedó inicializado el contenido proyectado (<ng-content>) |
Una |
ngAfterContentChecked |
Tras cada revisión del contenido proyectado | Muchas |
ngAfterViewInit |
Quedaron inicializados la vista del componente y sus hijos | Una |
ngAfterViewChecked |
Tras cada revisión de la vista | Muchas |
afterNextRender |
La próxima vez que la página se dibuja en el DOM | Una |
afterEveryRender |
Cada vez que la página se dibuja en el DOM | Muchas |
onCleanup |
Antes de que el effect se repita y cuando se destruye |
Cada limpieza |
ngOnDestroy |
Justo antes de que Angular destruya el componente | Una |
Cómo funciona por dentro¶
Piensa en un restaurante. El constructor es abrir la mesa; ngOnInit
es cuando el mesero ya tiene tu pedido y puede empezar a trabajar;
ngDoCheck es el mesero pasando a ver si cambió algo en tu mesa. Los hooks
After… son «ya se sirvieron los platos». La comparación se queda corta en
algo: el mesero pasa cuando él quiere, y Angular revisa la página cada vez que
algo puede haber cambiado, sin que tú lo pidas.
Tres ideas que conviene tener claras:
- El constructor no es un hook. Es de TypeScript, no de Angular. Corre
primero y todavía no hay inputs asignados; por eso el trabajo que depende de
ellos va en
ngOnInit. ngOnChangesnecesita un input. Se ejecuta cuando cambia una propiedad que el componente padre le pasa.HomePageComponentno tiene ningúninput(), así que ese hook nunca se ejecuta aquí. Para verlo hay que darle un input al componente y cambiar su valor desde el padre; el Módulo LH2 lo hace con el componente del título.- Salir de la página destruye el componente. Si vas a
Abouty vuelves aHome, Angular crea una instancia nueva: el constructor yngOnInitcorren otra vez desde cero. Antes de destruirla correngOnDestroy, que sirve para limpiar (cancelar suscripciones, quitar listeners). effectyonCleanupvan juntos. Eleffectse ejecuta cuando cambia una señal que lee. Lo que registres cononCleanupse ejecuta justo antes de la siguiente ejecución, así que sirve para cancelar lo que la anterior dejó pendiente (unsetTimeout, por ejemplo).afterNextRenderyafterEveryRenderesperan al DOM. Corren cuando la página ya está dibujada, por eso son el lugar para medir un elemento. La primera corre una vez; la segunda, en cada render, así que debe ser ligera.
Errores comunes¶
- Hacer en el constructor lo que debe ir en
ngOnInit. Pedir datos o leer inputs en el constructor falla, porque aún no están asignados. - Escribir trabajo pesado en
ngDoCheck,ngAfterContentCheckedongAfterViewChecked. Corren en cada revisión; un cálculo largo ahí vuelve lenta toda la página. - Esperar
ngOnChangessin tener inputs. No se ejecuta, y no es un error. - Medir el DOM en
ngOnInito en uneffect. Todavía no está dibujado; para eso estánafterNextRenderyafterEveryRender. - Olvidar el
implements. Si escribesngOnInten vez dengOnInit, Angular nunca lo llama y nada avisa; conimplements OnInitel compilador sí avisa.
Resumen¶
- Orden de arranque: constructor,
ngOnInit,ngDoCheck,effect,ngAfterContentInit,ngAfterContentChecked,ngAfterViewInit,ngAfterViewChecked,afterNextRender,afterEveryRender. ngOnChangessolo corre si el componente tiene inputs, y lo hace antes dengOnInit.- Los
…Initcorren una vez; los…CheckyngDoCheck, en cada revisión. - Para limpiar:
ngOnDestroyal destruir el componente yonCleanupen cada ejecución de uneffect.
Preguntas de entrevista¶
1. ¿Qué diferencia hay entre el constructor y ngOnInit?¶
Respuesta:
El constructor lo llama JavaScript al crear la instancia y sirve para dejar
la clase lista; ngOnInit lo llama Angular una vez que los inputs ya tienen
valor. Todo lo que dependa de un input, de una petición inicial o de la
plantilla va en ngOnInit, nunca en el constructor.
2. ¿Por qué ngOnChanges no se ejecuta en HomePageComponent?¶
Respuesta:
Porque ese hook solo se dispara cuando cambia un input recibido del padre, y
este componente no tiene ninguno. Si se le agrega un input() y el padre cambia
su valor, ngOnChanges se ejecuta, y lo hace también una vez justo antes de
ngOnInit.
3. ¿Qué hooks se repiten y por qué hay que tener cuidado con ellos?¶
Respuesta:
ngDoCheck, ngAfterContentChecked y ngAfterViewChecked corren en cada
revisión de cambios. Si el código que pones ahí es lento o modifica el estado,
retrasa toda la pantalla; deben ser muy livianos.
4. ¿Qué pasa con el componente al ir a otra ruta y volver?¶
Respuesta:
El router destruye el componente al salir de la ruta y crea uno nuevo al volver. Se repiten el constructor y toda la cadena de hooks desde el principio, y el estado que estaba en la instancia anterior se pierde.
5. ¿Para qué sirve declarar implements OnInit si Angular llama al método igual?¶
Respuesta:
Angular busca el método por su nombre, así que funciona sin la interfaz. La interfaz sirve para que el compilador revise que el método existe y está bien escrito; sin ella, un error de tipeo hace que el hook simplemente no se llame.
6. ¿Cuál es la diferencia entre afterNextRender y afterEveryRender?¶
Respuesta:
afterNextRender corre una sola vez, la próxima vez que la página se dibuja;
afterEveryRender corre después de cada dibujo. Las dos esperan a que el DOM
esté listo, así que sirven para medirlo o tocarlo, y la segunda debe ser muy
ligera porque se repite.
7. ¿Para qué sirve onCleanup dentro de un effect?¶
Respuesta:
Para registrar una limpieza que Angular ejecuta antes de volver a correr el
effect y cuando el effect se destruye. Es el equivalente a ngOnDestroy
pero por ejecución: cancela el setTimeout o la petición que la ejecución
anterior dejó pendiente.
Para ampliar: ciclo de vida de un componente.
Módulo LH2: ngOnChanges y los inputs¶
El problema¶
Un componente hijo recibe datos de su padre con un input(). Cuando el padre
cambia ese dato, el hijo a veces necesita saber qué cambió y cuál era el
valor anterior: para compararlos, para registrar el cambio o para reaccionar
solo si de verdad es distinto. ngOnChanges es el hook que te lo cuenta.
El código del repositorio¶
El hijo es el componente del título,
title.component.ts.
Tiene tres inputs y escribe en la consola todo lo que recibe ngOnChanges:
export class TitleComponent {
title = input<string>()
subTitle = input<string>()
link = input<string>()
ngOnChanges(changes: SimpleChanges) {
console.log('ngOnChanges called with changes:', changes)
for (const inputName in changes) {
const inputValues = changes[inputName]
console.log(`Previous value for ${inputName} ==> changed:`, inputValues.previousValue)
console.log(`Current value for ${inputName} ==> changed:`, inputValues.currentValue)
console.log(`Is first change for ${inputName}? ==>`, inputValues.firstChange)
}
}
}
(Aquí se omiten los comentarios y las líneas de separación de la consola.)
El padre, HomePageComponent, tiene una señal con un contador y un computed
que arma el texto del subtítulo. Un botón sube el contador:
onChangesCount = signal(0)
onChangesSubTitle = computed(() => `Input changed ${this.onChangesCount()} times`)
changeOnChangesInput() {
this.onChangesCount.update((count) => count + 1)
}
En la plantilla, el subtítulo del bloque «OnChanges» es el input que cambia:
<app-title title="OnChanges" [subTitle]="onChangesSubTitle()" link="{{ docs.ONCHANGES }}" />
<button (click)="changeOnChangesInput()">Change the input</button>
Qué se ve en la consola¶
Al abrir la página, cada título (hay uno por bloque) escribe un
ngOnChanges con todos sus inputs, porque todos son nuevos:
Previous value for subTitle ==> changed: undefined
Current value for subTitle ==> changed: Input changed 0 times
Is first change for subTitle? ==> true
Al pulsar el botón, el título del bloque «OnChanges» escribe solo el input que
cambió (title y link no aparecen porque siguen igual):
Previous value for subTitle ==> changed: Input changed 0 times
Current value for subTitle ==> changed: Input changed 1 times
Is first change for subTitle? ==> false
Un clic más y el anterior pasa a ser «1 times» y el actual «2 times».
Cómo funciona por dentro¶
Cada vez que Angular revisa la plantilla del padre, mira el valor que le pasa a
cada input y lo compara con el que le pasó la vez anterior. Si es distinto,
guarda el cambio en un objeto (SimpleChanges) y llama a ngOnChanges antes
de revisar el hijo. Por eso:
changessolo trae lo que cambió. Es un objeto con una entrada por input cambiado; la clave es el nombre del input.- Cada entrada (
SimpleChange) tiene tres datos:previousValue,currentValueyfirstChange. - La primera vez corre antes de
ngOnInit. AhípreviousValueesundefinedyfirstChangeestrue: es el valor con que Angular crea el componente. - Compara por referencia. Si el padre pasa un objeto y luego lo modifica por dentro (sin crear uno nuevo), Angular ve el mismo objeto y no avisa.
- Funciona también con
input()de señales, como aquí. Para reaccionar a un input de señal, lo habitual hoy es leerlo en uncomputedo uneffect;ngOnChangessigue siendo útil cuando necesitas el valor anterior.
Errores comunes¶
- Esperar
ngOnChangespor un campo que cambia dentro del propio componente. Solo reacciona a inputs que el padre asigna. - Mutar un objeto o un arreglo que se pasa como input. La referencia no cambia y el hook no se ejecuta; hay que pasar uno nuevo.
- Leer
previousValueen la primera llamada. Esundefined; revisafirstChangeantes de compararlo. - Poner trabajo pesado dentro. Corre cada vez que cambia un input.
Resumen¶
ngOnChanges(changes: SimpleChanges)corre antes dengOnInity cada vez que el padre cambia un input.changestrae solo los inputs que cambiaron, conpreviousValue,currentValueyfirstChange.- Compara por referencia y solo mira inputs, no campos internos.
Preguntas de entrevista¶
1. ¿Qué recibe ngOnChanges y qué contiene?¶
Respuesta:
Recibe un objeto SimpleChanges con una entrada por cada input que cambió en
esa revisión. Cada entrada es un SimpleChange con el valor anterior
(previousValue), el nuevo (currentValue) y si es la primera vez
(firstChange).
2. ¿Cuándo se ejecuta por primera vez ngOnChanges?¶
Respuesta:
Justo antes de ngOnInit, cuando Angular asigna los valores iniciales de los
inputs. En esa llamada firstChange es true y previousValue es undefined.
Si el componente no tiene inputs, nunca se ejecuta.
3. ¿Por qué ngOnChanges no se dispara si modifico un objeto que paso como input?¶
Respuesta:
Porque Angular compara por referencia: si el objeto es el mismo, para él no hay cambio, aunque por dentro tenga otros valores. Para que el hook se ejecute hay que pasar un objeto nuevo (por ejemplo con el operador de propagación).
4. Si ya uso input() de señales, ¿sigo necesitando ngOnChanges?¶
Respuesta:
Normalmente no: se lee el input dentro de un computed (para derivar un valor)
o de un effect (para un efecto secundario), y se actualizan solos. El hook
sigue funcionando con inputs de señales, y es útil si necesitas comparar el
valor anterior con el nuevo.
Para ampliar: ngOnChanges en la documentación.
Módulo LH3: propiedad normal y señal¶
El problema¶
En una app con zone.js, casi cualquier cosa que cambie un campo (un clic, un
setTimeout, una petición) hace que Angular vuelva a revisar la pantalla. Esta
app no usa zone.js (zoneless): Angular solo revisa cuando algo se lo
pide. Entonces surge una duda concreta: si cambio un campo normal desde un
setTimeout, ¿se ve el cambio? ¿Y si cambio una señal?
El código del repositorio¶
En HomePageComponent hay dos propiedades con el mismo valor inicial: una
normal y una señal. Dos botones las cambian, y un setTimeout del constructor
las cambia a los dos segundos:
propertyValue = 'Homero Thompson'
propertyValueChanged = 'Homero Simpson'
tradProperty = this.propertyValue
signalProperty = signal(this.propertyValue)
constructor() {
setTimeout(() => {
ConsoleLogger.format('setTimeout', HOME_TEXTS.SET_TIMEOUT_EXAMPLE)
this.signalProperty.set(this.propertyValueChanged)
this.tradProperty = this.propertyValueChanged
}, 2000)
}
changeTradProperty() {
this.tradProperty =
this.tradProperty === this.propertyValue ? this.propertyValueChanged : this.propertyValue
}
changeSignalProperty() {
this.signalProperty.set(
this.signalProperty() === this.propertyValue ? this.propertyValueChanged : this.propertyValue,
)
}
(Aquí se omite el resto del constructor y los comentarios.) La plantilla muestra
las dos ({{ tradProperty }} y {{ signalProperty() }}), los botones, la lista
de hooks que se repiten y, en un aviso, el texto de HOME_TEXTS.SET_TIMEOUT_EXAMPLE.
Qué se ve¶
| Cambio | ¿Se actualiza la pantalla? |
|---|---|
| Botón «Change Traditional Property» | Sí, porque el clic le pide a Angular una revisión |
| Botón «Change Signal Property» | Sí |
setTimeout que solo cambia la propiedad normal |
No: la pantalla sigue con el valor anterior |
setTimeout que cambia la señal y la normal |
Sí, y se ven las dos al valor nuevo |
Cada cambio que sí se refleja vuelve a ejecutar ngDoCheck,
ngAfterContentChecked, ngAfterViewChecked y afterEveryRender. No se
ejecutan ngOnInit, ngOnChanges, afterNextRender ni el effect (que no
lee esa señal).
Cómo funciona por dentro¶
Piensa en el timbre de una casa. Con zone.js, cada cosa que ocurre (un
setTimeout, una petición) toca el timbre sola. Sin zone.js, nadie lo toca,
salvo que el cambio venga de un evento de la plantilla (un clic) o de una
señal.
- Un campo normal es invisible para Angular. Cambia en memoria, pero nada avisa. Con un clic funciona porque el evento ya pide una revisión.
signal.set()marca la vista como pendiente. Angular sabe qué plantillas leen esa señal y las refresca, venga el cambio de donde venga.- Al refrescar se redibuja toda la plantilla. Por eso, cuando la señal
cambia, también se ve el valor nuevo de la propiedad normal: ya había
cambiado en memoria y solo esperaba a que la pantalla se redibujara. Si
quitas la línea del
signal.set(), la pantalla sigue con el valor viejo.
La comparación se queda corta en algo: la propiedad normal no «se actualiza sola» con la señal; solo aprovecha que la pantalla se redibuja. Si nada la redibuja, el cambio no se ve.
Errores comunes¶
- Cambiar un campo normal desde un
setTimeout, una promesa o una suscripción y esperar que se vea en una app sinzone.js. Usa una señal. - Creer que la propiedad normal «funciona» porque se ve bien en un ejemplo. Puede verse bien solo porque otra señal provocó el redibujado.
- Pensar que el
effectcorre en cada cambio. Solo corre cuando cambia una señal que él lee.
Resumen¶
- Sin
zone.js, Angular revisa la pantalla por un evento de la plantilla, por una señal o por otra señal de aviso explícita; unsetTimeoutno basta. signal.set()marca la vista y la refresca; el refresco redibuja toda la plantilla.- Con cada cambio que se refleja se repiten
ngDoCheck,ngAfterContentChecked,ngAfterViewCheckedyafterEveryRender.
Preguntas de entrevista¶
1. ¿Por qué cambiar un campo normal dentro de un setTimeout no actualiza la pantalla sin zone.js?¶
Respuesta:
Porque nada le avisa a Angular que algo cambió. Con zone.js, la librería
intercepta el setTimeout y dispara una revisión al terminar; sin ella, Angular
solo revisa cuando se lo piden un evento de la plantilla, una señal o una
llamada explícita.
2. ¿Por qué, si en el mismo setTimeout cambio también una señal, se ve el valor nuevo de la propiedad normal?¶
Respuesta:
Porque signal.set() marca la vista como pendiente y la revisión siguiente
redibuja toda la plantilla, no solo el pedazo que usa la señal. La propiedad
normal ya tenía el valor nuevo en memoria; lo único que faltaba era que la
pantalla se redibujara.
3. ¿Qué ventaja tiene una señal sobre una propiedad normal en una app sin zone.js?¶
Respuesta:
Angular sabe qué plantillas leen la señal, así que se refresca exactamente lo que depende de ella desde cualquier origen (un evento, un temporizador, una petición). Con una propiedad normal, el cambio solo se ve si algo más provoca la revisión.
Para ampliar: Angular sin zone.js.