A · Fundamentals → Module 03
Module 03 · Questions 13–17

Component Lifecycle

Five hooks worth knowing, what each is for, and the order — which is the part people get wrong.

Q13

The lifecycle hooks and their order

CREATED constructor inject dependencies. Inputs are NOT set yet. ngOnChanges inputs set — runs BEFORE ngOnInit ngOnInit initialise: load data, subscribe ngDoCheck every change detection cycle (rarely used) ngAfterContentInit projected content ready ngAfterViewInit template + child components rendered RUNNING ngOnChanges again, each time an @Input changes ngDoCheck / ngAfterViewChecked … DESTROYED ngOnDestroy unsubscribe, clear timers
AngularReact
ngOnInituseEffect(fn, [])
ngOnChangesuseEffect(fn, [prop])
ngOnDestroythe cleanup function returned from useEffect
The four that matter

You'll use ngOnInit, ngOnChanges, ngAfterViewInit and ngOnDestroy. Don't recite all eight — name these four and say what each is for.

Q14

ngOnInit vs the constructor

The rule

Constructor = dependency injection only. ngOnInit = everything else, because that's when the inputs exist.

export class OrderDetailComponent implements OnInit {
  @Input() orderId!: number;
  order?: Order;

  constructor(private api: OrderService) {
    console.log(this.orderId);        // undefined — inputs aren't set yet
  }

  ngOnInit() {
    console.log(this.orderId);        // 42 — now it's here
    this.api.get(this.orderId).subscribe(o => this.order = o);
  }
}
ConstructorInject services. Nothing else.
ngOnInitRead inputs, load data, set up subscriptions and forms.
Say this

The constructor runs when the class is created, before Angular has set the @Input values, so anything that depends on an input has to go in ngOnInit. I keep the constructor to dependency injection only — it also makes the component easier to test, because constructing it doesn't kick off an API call.

Q15

ngOnChanges

The parent changes orderId from 1 to 2. Which hooks run, and in what order?
export class OrderDetailComponent implements OnInit, OnChanges {
  @Input() orderId!: number;

  ngOnChanges(changes: SimpleChanges) {
    console.log('changes', changes['orderId']?.currentValue);
  }

  ngOnInit() {
    console.log('init', this.orderId);
    this.load(this.orderId);
  }
}
First render (orderId = 1): changes 1 ← ngOnChanges runs FIRST init 1 Parent changes it to 2: changes 2 ← only ngOnChanges ← ngOnInit does NOT run again So load() is never called for order 2. The component shows order 1 for ever.

The fix: put the reload in ngOnChanges, not ngOnInit — or if the id comes from the URL, subscribe to paramMap instead (Q29). This is one of the most common Angular bugs.

ngOnChanges(changes: SimpleChanges) {
  if (changes['orderId'] && !changes['orderId'].firstChange) {
    this.load(this.orderId);
  }
}

SimpleChanges gives you previousValue, currentValue and firstChange for each changed input.

⚠ It only fires on reference change

Mutating an object you passed in (this.user.name = 'x') does not trigger ngOnChanges — the reference is the same. Replace the object instead. This matters even more with OnPush (Q46).

Q16ngAfterViewInit

Runs once, after the component's template and its child components have rendered. It's the only place a @ViewChild is reliably available.

@ViewChild('search') searchInput!: ElementRef;

ngOnInit()        { this.searchInput; }   // undefined — the view doesn't exist yet
ngAfterViewInit() { this.searchInput.nativeElement.focus(); }   // works

Use it for: focusing an input, initialising a chart or map library, measuring an element.

⚠ ExpressionChangedAfterItHasBeenCheckedError

Changing a value that's bound in the template from inside ngAfterViewInit throws this in development mode — Angular has already checked that value this cycle. Wrap it in setTimeout(), or move the work to ngOnInit.

Q17

ngOnDestroy

In one line

The last hook before the component is removed — and the only place to clean up what you started.

export class LiveComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();
  private timer?: any;

  ngOnInit() {
    this.feed.updates$
        .pipe(takeUntil(this.destroy$))
        .subscribe(u => this.rows = u);

    this.timer = setInterval(() => this.refresh(), 30000);
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
    clearInterval(this.timer);
  }
}
Clean upOr else
SubscriptionsThey keep running and hold the component in memory
setInterval / setTimeoutThe timer fires after the user has navigated away
Manual DOM listenersSame — plus they stack up each time the component is created

Anything using the async pipe needs none of this — it unsubscribes itself. That's the main reason to prefer it (Q44).

Say this

ngOnDestroy is where I unsubscribe and clear timers. A subscription that outlives its component keeps the component in memory and keeps running — navigate in and out of that screen twenty times and you have twenty live subscriptions. I use takeUntil with a destroy subject, or better, the async pipe so there's nothing to clean up.