Lifecycle and Change Detection
|
This section documents modern, standalone Angular — signals, the built-in This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted. This section’s bibliography lists the reference material consulted while preparing these pages. |
A component moves through a fixed sequence of lifecycle hooks from creation to destruction, and Angular keeps the DOM in sync with component state through change detection. Modern signal-based code needs fewer hooks, but understanding both is essential for performance work.
Lifecycle hooks
Implement the matching interface (OnInit, OnDestroy, …) and Angular calls the method at the right time.
| Hook | When it runs |
|---|---|
|
Before |
|
Once, after the first |
|
Every change-detection run. Custom dirty-checking only; keep it cheap. |
|
Once, after projected content ( |
|
After every check of projected content. |
|
Once, after the component’s own view and child views exist — view queries are resolved. |
|
After every check of the component’s view. |
|
Just before Angular destroys the component. Unsubscribe, clear timers. |
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
@Component({ selector: 'app-clock', template: '{{ now }}' })
export class Clock implements OnInit, OnDestroy {
now = '';
private id?: ReturnType<typeof setInterval>;
constructor() {
// DI and field setup only -- inputs are NOT available yet.
}
ngOnInit(): void {
this.id = setInterval(() => (this.now = new Date().toLocaleTimeString()), 1000);
}
ngOnDestroy(): void {
clearInterval(this.id);
}
}
constructor vs. ngOnInit: the constructor runs at class instantiation, before Angular sets inputs or
resolves queries — use it only for inject() and simple field initialisation. Put anything that reads inputs
or starts work in ngOnInit. See Component lifecycle.
Cleanup: DestroyRef and render callbacks
DestroyRef.onDestroy(fn) registers teardown without implementing OnDestroy, and
takeUntilDestroyed() (from @angular/core/rxjs-interop) completes an observable when the injection
context is destroyed — the cleanest way to end a subscription.
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({ selector: 'app-ticker', template: '{{ tick }}' })
export class Ticker {
tick = 0;
private readonly destroyRef = inject(DestroyRef);
constructor() {
interval(1000)
.pipe(takeUntilDestroyed()) // auto-unsubscribe on destroy
.subscribe(() => (this.tick++));
this.destroyRef.onDestroy(() => console.log('ticker gone'));
}
}
afterNextRender(fn) runs a callback once after the next render, and afterRender(fn) after every
render — both browser-only, for measuring or manipulating the real DOM after Angular has written to it. See
afterNextRender.
How change detection works
Change detection is Angular checking each component’s template bindings and updating the DOM where a bound value changed. It runs top-down from the root component.
-
Zone.js (the traditional model) monkey-patches async APIs —
addEventListener,setTimeout,fetch, promises — and tells Angular to run change detection after each such task completes. It is the reason a click handler that mutates a field "just updates the view". -
ChangeDetectionStrategy.Defaultchecks every component on every pass.ChangeDetectionStrategy.OnPushchecks a component only when: an@Input()reference changes, an event fires inside it, a bound signal it reads changes, anasyncpipe in its template emits, orChangeDetectorRef.markForCheck()is called.OnPusheverywhere is the main lever for large-app performance. -
ChangeDetectorRefgives manual control:markForCheck()schedules this component (and its ancestors) to be checked next pass;detach()removes it from the tree entirely;detectChanges()runs a synchronous check of it now;reattach()reversesdetach(). -
Signals integrate directly: when a signal read in a template changes, Angular marks exactly that component dirty — no
markForCheckneeded, and it works the same underOnPush. -
Zoneless change detection (
provideZonelessChangeDetection(), stable since v20) drops Zone.js entirely. Angular then schedules a check only from explicit signals: signal writes,asyncpipe emissions,markForCheck(), and template event bindings. Removezone.jsfrompolyfillsand prefer signals for all reactive state.
// main.ts -- opt into zoneless
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { App } from './app/app';
bootstrapApplication(App, {
providers: [provideZonelessChangeDetection()],
});
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
@Component({
selector: 'app-row',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '{{ label() }}',
})
export class Row {
readonly label = input.required<string>(); // signal input -> marks Row dirty on change
}
References: Runtime performance and Zoneless.
One change-detection pass
or Default strategy?"} D -- "yes" --> E["Check template bindings,
update changed DOM"] D -- "no (OnPush, not dirty)" --> F["Skip component
and its subtree"] E --> G["Recurse into children"] F --> G G --> H["Browser paints"]