All articles

Angular Lifecycle Hooks Explained — with Diagram and Firing Order

Every Angular lifecycle hook with a clear diagram and the real firing order, proven with logged output: ngOnInit vs constructor, ngOnChanges, ngAfterViewInit, and the ngOnDestroy cleanup that prevents leaks.

0 · log in to like, save & follow Share on LinkedIn Share on X

"Why is my input undefined in the constructor?" and "why does my app leak memory?" have the same answer: the component lifecycle. Angular creates, checks, and destroys components on a strict schedule, and each phase has a hook. This article walks the hooks in their real firing order — proven by a demo component that logs every one — and pins down the two hooks that matter daily, on Angular 20 with verified output.

Angular Component Lifecycle Hooks Explained

The order, observed

Angular component lifecycle diagram — hook firing order

The demo component implements every major hook and records each call. Actual sequence from the running app:

constructor — DI done, inputs NOT set yet
ngOnChanges — productId unset -> 1
ngOnInit — inputs ready; fetch data, start subscriptions here
ngAfterViewInit — the DOM below me exists now
ngOnChanges — productId 1 -> 2        (parent changed the input)
ngOnDestroy — cleanup ran             (parent removed the component)

That log answers the classic confusions directly: inputs are not available in the constructor (they arrive before ngOnChanges/ngOnInit); the DOM isn't queryable until ngAfterViewInit; and destruction is your only cleanup chance.

The hooks that earn their keep

ngOnInit — initialization. Runs once, after the first ngOnChanges, with all inputs set. This is where data loading and subscription setup belong:

ngOnInit(): void {
  this.timer = setInterval(() => this.refresh(), 60_000);
}

Why not the constructor? Two reasons: inputs aren't set there, and constructors should be cheap wiring (DI) — testability suffers when construction has side effects.

ngOnDestroy — cleanup. The leak preventer:

ngOnDestroy(): void {
  clearInterval(this.timer);
}

Intervals, manual RxJS subscriptions, WebSocket connections, and DOM listeners added outside Angular all outlive the component unless you end them here. A routed-away component that kept its interval still runs it — forever, once per navigation. Modern alternatives shrink this hook: takeUntilDestroyed() auto-completes streams, and DestroyRef.onDestroy(...) registers cleanup without implementing the interface — but the concept is identical.

ngOnChanges — reacting to input updates. Fires before ngOnInit and again on every input change, with a SimpleChanges map:

ngOnChanges(changes: SimpleChanges): void {
  const c = changes['productId'];
  console.log(`productId ${c.previousValue ?? 'unset'} -> ${c.currentValue}`);
}

The classic use — refetch when an id input changes — has a cleaner modern form: make the input a signal (productId = input.required<number>()) and derive with computed/effect; the reaction is declared once instead of dug out of a changes map. ngOnChanges remains what you'll read in existing codebases.

ngAfterViewInit — the DOM exists. @ViewChild references resolve here; third-party widgets (charts, maps) initialize here. Setting state that the template renders here triggers the famous ExpressionChangedAfterItHasBeenCheckedError in dev mode — that error is the framework telling you the value changed after the check pass; move the change earlier (or queueMicrotask it).

The hooks you'll rarely touch

ngDoCheck, ngAfterContentInit/Checked, and ngAfterViewChecked exist for custom change detection and content-projection timing. The *Checked hooks run on every change-detection cycle — putting real work in them is a performance foot-gun. If you're reaching for ngDoCheck, a computed signal almost certainly expresses the intent better.

Destruction is recursive — and conditional rendering triggers it

@if removing a component, @for dropping an item, and router navigation all destroy the affected component and its whole subtree, firing ngOnDestroy bottom-up. The demo's toggle button proves it:

<button (click)="showLifecycle.set(!showLifecycle())">Toggle component</button>
@if (showLifecycle()) {
  <app-lifecycle-demo [productId]="productId()" />
}

Toggle off: destroy runs, interval cleared. Toggle on: a fresh instance — constructor, ngOnChanges, ngOnInit again, state reset. Components hidden with CSS ([hidden]) keep their instance; components removed with @if don't. Choosing between those two is choosing whether state survives — make it consciously.

Working rules

  1. Constructor = DI only. ngOnInit = setup. ngOnDestroy = teardown, always paired with whatever ngOnInit started.
  2. Reacting to inputs: prefer signal inputs + computed; use ngOnChanges in legacy code.
  3. Touch the DOM in ngAfterViewInit, not earlier.
  4. Anything with Checked in the name runs constantly — keep it near-empty.

The demo component with every hook logging — plus the toggle and input-change buttons to trigger them — is in the companion repository, part of the same workspace as the components tutorial: npm install && ng serve, open the console, and watch the order happen.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.