Skip to main content

component lifecycle methods in Angular

Angular component lifecycle hooks are predefined methods Angular calls at specific points during a component's creation, updates, and removal from the DOM.

Theory

TL;DR

  • Hooks fire in a fixed order: ngOnChangesngOnInit → ... → ngOnDestroy
  • Think of it like a construction checklist: foundation laid (ngOnInit), wiring checked (ngAfterViewInit), final teardown (ngOnDestroy)
  • Use ngOnInit for data fetching and ngOnDestroy for cleanup (subscriptions, timers)
  • ngOnChanges fires before ngOnInit and on every @Input change after that
  • ngDoCheck runs on every change detection cycle — use it as a last resort

Quick example

typescript
import { Component, OnInit, OnDestroy, Input } from '@angular/core'; @Component({ selector: 'app-user', template: `<p>{{ user?.name }}</p>` }) export class UserComponent implements OnInit, OnDestroy { @Input() user: { name: string }; ngOnInit() { // Runs once after @Input properties are bound console.log('Component ready, user:', this.user); } ngOnDestroy() { // Runs before the component leaves the DOM console.log('Cleaning up'); } } // On create: "Component ready, user: {name: 'Alice'}" // On destroy: "Cleaning up"

In ngOnInit, @Input values are already set. In the constructor, they are not.

Hook execution order

Hooks fire in this sequence on component initialization:

HookWhen triggeredTypical use
ngOnChangesInput property binds or changesReact to parent updates, log input diffs
ngOnInitAfter first ngOnChanges, bindings completeFetch data, init services
ngDoCheckEvery change detection cycleCustom equality checks
ngAfterContentInitProjected content (ng-content) readyQuery projected components
ngAfterContentCheckedContent view checkedPost-content mutation logic
ngAfterViewInitComponent + children views renderedAccess ViewChild, start animations
ngAfterViewCheckedView checkedFine-tune after full render
ngOnDestroyBefore DOM removalCleanup subscriptions, timers

After the first full pass, Angular cycles through ngDoCheck, ngAfterContentChecked, and ngAfterViewChecked on every subsequent change detection run.

Key difference: ngOnInit vs constructor

The constructor runs during dependency injection, before Angular binds any inputs. ngOnInit runs after the first change detection pass, when @Input values are already set. Putting a data fetch in the constructor means your service call runs with no context from the parent yet. That is the most common mistake you will see in Angular code reviews.

When to use each hook

  • Data fetch or service call: ngOnInit
  • React to parent @Input changes: ngOnChanges with a SimpleChanges check
  • Access ViewChild or start DOM animations: ngAfterViewInit
  • Query projected ng-content children: ngAfterContentInit
  • Cleanup (unsubscribe, cancel timers, close WebSocket): ngOnDestroy
  • Custom detection when OnPush misses a mutation: ngDoCheck (last resort only)

ngOnInit and ngOnDestroy cover 90% of real-world cases. The content and view hooks only matter when you work with ng-content or ViewChild.

How Angular runs hooks internally

Angular's Ivy renderer (default since Angular 9) creates a component instance, binds @Input via property setters (which triggers ngOnChanges), then runs change detection through Zone.js-patched async tasks. Hooks fire as the renderer traverses the component tree top-down. Post-render phases like ngAfterViewInit are queued as microtasks after the view is fully built. Ivy pre-generates lifecycle calls at compile time, which cuts runtime overhead compared to the older ViewEngine.

For a parent-child tree: parent ngOnInit fires first, then the child goes through its full cycle. Parent ngAfterViewInit fires only after all children have completed their view initialization.

Common mistakes

Fetching data in the constructor:

typescript
// Wrong - @Input values are not available yet constructor(private svc: DataService) { this.svc.getData(this.userId); // this.userId is undefined here } // Right ngOnInit() { this.svc.getData(this.userId); // @Input() userId is bound }

Accessing ViewChild before ngAfterViewInit:

typescript
// Wrong - view is not rendered yet ngOnInit() { console.log(this.myElement.nativeElement); // null } // Right ngAfterViewInit() { console.log(this.myElement.nativeElement); // works }

Skipping ngOnDestroy unsubscribe:

typescript
// Wrong - subscription lives forever, memory leak on every route change ngOnInit() { interval(1000).subscribe(v => this.data = v); } // Right - takeUntil pattern private destroy$ = new Subject<void>(); ngOnInit() { interval(1000).pipe(takeUntil(this.destroy$)).subscribe(v => this.data = v); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }

ngOnChanges without checking which input changed:

typescript
// Runs full reload on every trivial input change ngOnChanges() { this.loadData(); } // Check first ngOnChanges(changes: SimpleChanges) { if (changes['userId'] && !changes['userId'].firstChange) { this.loadData(); } }

Real-world usage

  • Angular Material dialogs: ngAfterViewInit starts animations once modal content is in the DOM
  • NGRX/NGXS stores: ngOnDestroy cleans up selectors and effects to prevent stale subscriptions
  • PrimeNG tables: ngAfterViewChecked handles virtual scroll resize after each render cycle
  • Route-based components: ngOnInit loads lazy data after route resolvers finish
  • OnPush components with mutable arrays: ngDoCheck + markForCheck() when reference equality fails

Follow-up questions

Q: What is the difference between the constructor and ngOnInit?
A: Constructor runs during DI, before Angular binds any @Input properties. ngOnInit runs after the first change detection pass, so inputs are available. Use the constructor only for injecting dependencies.

Q: When does ngOnChanges fire relative to ngOnInit?
A: ngOnChanges fires first, before ngOnInit, whenever an @Input property is set or updated. If a component has no @Input, ngOnChanges never fires at all.

Q: Why is ngDoCheck a last resort?
A: It runs on every change detection cycle, even when nothing changed. That can mean hundreds of calls per second. Prefer OnPush with immutable data or Angular signals (v16+) before reaching for ngDoCheck.

Q: How does hook order work in a parent-child component tree?
A: Parent ngOnInit fires, then the child goes through its full lifecycle. Parent ngAfterViewInit fires only after all children have completed their own view initialization.

Q: OnPush component with a mutable @Input array - why does the UI stop updating and how do you fix it?
A: OnPush skips change detection if the input reference has not changed. Mutating an array with .push() or .splice() keeps the same reference, so Angular skips the component entirely. Fix with ngDoCheck + cdRef.markForCheck(), or better: replace the array with a new reference on every update.

Examples

Basic: tracking input changes with ngOnChanges

typescript
@Component({ selector: 'app-counter', template: '<button (click)="increment()">Count: {{ count }}</button>' }) export class CounterComponent implements OnChanges { @Input() max = 10; count = 0; ngOnChanges(changes: SimpleChanges) { if (changes['max']) { // Only react to the specific input that changed console.log('Max changed to', changes['max'].currentValue); if (this.count > changes['max'].currentValue) { this.count = 0; // reset if over new limit } } } increment() { this.count++; } } // Parent changes @Input max from 10 to 5 → "Max changed to 5" // If count was 7, it resets to 0

SimpleChanges gives you both currentValue and previousValue. Without it you are reacting blindly to every change detection cycle.

Intermediate: data loading with cleanup

This pattern covers the most common production scenario: fetch on init, cancel on destroy. It comes directly from Angular's Tour of Heroes architecture.

typescript
@Component({ template: ` <div *ngIf="hero$ | async as hero"> {{ hero.name }} </div> ` }) export class HeroDetailComponent implements OnInit, OnDestroy { hero$: Observable<Hero>; private destroy$ = new Subject<void>(); constructor( private heroService: HeroService, private route: ActivatedRoute ) {} ngOnInit() { const id = this.route.snapshot.paramMap.get('id'); this.hero$ = this.heroService.getHero(id).pipe( takeUntil(this.destroy$) ); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); // prevents memory leak on route change } } // Hero loads on component init // Subscription cancels cleanly when user navigates away

Advanced: ngDoCheck with OnPush for mutable array detection

typescript
@Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: `<ul><li *ngFor="let item of items">{{ item }}</li></ul>` }) export class ItemListComponent implements DoCheck { @Input() items: string[] = []; private prevLength = 0; constructor(private cdRef: ChangeDetectorRef) {} ngDoCheck() { // OnPush misses array.push() because the reference stays the same if (this.items.length !== this.prevLength) { this.prevLength = this.items.length; this.cdRef.markForCheck(); // force re-render for this subtree } } } // Without ngDoCheck: UI freezes after array.push() // With it: rerenders on length change, but ngDoCheck runs every cycle (perf cost) // Better alternative: replace the array with a new reference on every update

In production I prefer the immutable update approach over ngDoCheck. The markForCheck path works, but every extra comparison you add to ngDoCheck eventually becomes a performance conversation during code review.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?