Lifecycle and Change Detection

This section documents modern, standalone Angular — signals, the built-in @if / @for / @switch control flow, @defer, typed reactive forms, provideHttpClient, functional guards and interceptors, and server-side rendering with hydration — as described by the official documentation at angular.dev, which is the reference these pages are written and verified against.

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

ngOnChanges(changes)

Before ngOnInit and again whenever a decorator @Input() changes. Signal inputs do not trigger it — react with computed() / effect() instead.

ngOnInit()

Once, after the first ngOnChanges — inputs are set. Do initialisation here.

ngDoCheck()

Every change-detection run. Custom dirty-checking only; keep it cheap.

ngAfterContentInit()

Once, after projected content (<ng-content>) is initialised.

ngAfterContentChecked()

After every check of projected content.

ngAfterViewInit()

Once, after the component’s own view and child views exist — view queries are resolved.

ngAfterViewChecked()

After every check of the component’s view.

ngOnDestroy()

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.Default checks every component on every pass. ChangeDetectionStrategy.OnPush checks a component only when: an @Input() reference changes, an event fires inside it, a bound signal it reads changes, an async pipe in its template emits, or ChangeDetectorRef.markForCheck() is called. OnPush everywhere is the main lever for large-app performance.

  • ChangeDetectorRef gives 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() reverses detach().

  • Signals integrate directly: when a signal read in a template changes, Angular marks exactly that component dirty — no markForCheck needed, and it works the same under OnPush.

  • Zoneless change detection (provideZonelessChangeDetection(), stable since v20) drops Zone.js entirely. Angular then schedules a check only from explicit signals: signal writes, async pipe emissions, markForCheck(), and template event bindings. Remove zone.js from polyfills and 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

flowchart TD A["Event, async task, or signal write"] --> B["Angular schedules change detection"] B --> C["Traverse component tree from the root"] C --> D{"Component dirty
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"]