Angular Lifecycle Hooks

Every Angular component and directive moves through a predictable sequence of lifecycle hooks that let you run setup logic, react to input changes, and clean up before it's destroyed.

The Lifecycle Sequence

Angular calls a fixed set of methods on a component in a specific order, from creation to destruction. Implementing the corresponding interface (like OnInit) is optional but recommended, since it gives you compile-time checking that the method signature is correct.

  • ngOnChanges — runs before ngOnInit and again whenever a bound @Input() value changes.
  • ngOnInit — runs once, after the first ngOnChanges, when the component's inputs are set.
  • ngDoCheck — runs on every change detection cycle for custom change checks.
  • ngAfterContentInit / ngAfterContentChecked — run after projected content (ng-content) is initialized/checked.
  • ngAfterViewInit / ngAfterViewChecked — run after the component's own view (and child views) are initialized/checked.
  • ngOnDestroy — runs once, right before Angular removes the component, for cleanup.

ngOnInit for Initialization

ngOnInit is the right place for one-time setup work such as an initial data fetch. Avoid doing this in the constructor — constructors should stay lightweight and free of side effects like HTTP calls, since dependency injection is still being wired up at that point.

profile.component.ts

import { Component, OnInit, Injectable, inject, signal } from '@angular/core';
import { Observable, of } from 'rxjs';
import { bootstrapApplication } from '@angular/platform-browser';

interface User {
  id: number;
  name: string;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  getUser(id: number): Observable<User> {
    return of({ id, name: 'Ada Lovelace' });
  }
}

@Component({
  selector: 'app-profile',
  standalone: true,
  template: `<p>{{ name() }}</p>`
})
export class ProfileComponent implements OnInit {
  private userService = inject(UserService);
  protected name = signal('Loading...');

  ngOnInit(): void {
    console.log('ngOnInit called');
    this.userService.getUser(1).subscribe(user => this.name.set(user.name));
  }
}

bootstrapApplication(ProfileComponent).catch((err) => console.error(err));

ngOnChanges for Input Reactions

ngOnChanges receives a SimpleChanges map describing which @Input() properties changed, along with their previous and current values. It fires before the values are used anywhere else in the component, making it a good spot to recompute a derived value whenever an input updates.

price-tag.component.ts

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  selector: 'app-price-tag',
  standalone: true,
  template: `<p>Price: {{ formatted }}</p>`
})
export class PriceTagComponent implements OnChanges {
  @Input() amount = 0;
  @Input() currency = 'USD';
  formatted = '';

  ngOnChanges(changes: SimpleChanges): void {
    console.log('ngOnChanges', changes);
    if (changes['amount'] || changes['currency']) {
      this.formatted = new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: this.currency
      }).format(this.amount);
    }
  }
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PriceTagComponent],
  template: `<app-price-tag [amount]="19.99" [currency]="'USD'"></app-price-tag>`
})
export class AppComponent {}

bootstrapApplication(AppComponent).catch((err) => console.error(err));

Cleaning Up with ngOnDestroy

ngOnDestroy runs right before Angular tears the component down — for example when a router navigates away or an @if block removes it. Anything that would otherwise keep running in the background, like an RxJS subscription or a setInterval timer, must be cancelled here to prevent memory leaks.

live-clock.component.ts

import { Component, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  selector: 'app-live-clock',
  standalone: true,
  template: `<p>Seconds elapsed: {{ seconds }}</p>`
})
export class LiveClockComponent implements OnDestroy {
  seconds = 0;
  private sub: Subscription;

  constructor() {
    console.log('LiveClockComponent created');
    this.sub = interval(1000).subscribe(() => this.seconds++);
  }

  ngOnDestroy(): void {
    console.log('ngOnDestroy called - clearing subscription');
    this.sub.unsubscribe();
  }
}

bootstrapApplication(LiveClockComponent).catch((err) => console.error(err));
HookRuns whenTypical use
ngOnChangesBefore ngOnInit, and on every @Input() changeReact to new input values
ngOnInitOnce, after the first inputs are setInitial data fetch, one-time setup
ngAfterViewInitOnce, after the view (and child views) renderRead/measure DOM elements, third-party widget init
ngOnDestroyOnce, right before removalUnsubscribe, clear timers, detach listeners
Note: Modern Angular's signal-based effect() can replace some manual ngOnChanges/ngDoCheck logic — it automatically re-runs whenever any signal it reads changes, without you writing comparison code by hand.
Note: Forgetting ngOnDestroy cleanup is one of the most common sources of memory leaks in Angular apps — a subscription that outlives its component keeps updating state nobody can see, and keeps the component instance from being garbage collected.

Exercise: Angular Lifecycle Hooks

What differs between a component's constructor and ngOnInit()?