Component Lifecycle Hooks — ngOnInit, ngOnChanges and ngOnDestroy

Angular components have a well-defined lifecycle: they are created, rendered, updated when inputs change, and eventually destroyed. Lifecycle hooks let you run code at specific points in this lifecycle. ngOnInit is the most commonly used — it runs after Angular has set all input properties, making it the right place to load data from the API. ngOnDestroy is equally important — subscriptions and timers not cleaned up here cause memory leaks and ghost requests. Angular 16+ provides takeUntilDestroyed() as the modern, cleaner alternative to manual subscription management.

Lifecycle Hooks in Practice

import {
  Component, OnInit, OnChanges, OnDestroy, AfterViewInit,
  Input, SimpleChanges, inject, signal,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { PostsService } from './posts.service';

@Component({ selector: 'app-post-detail', standalone: true, template: '...' })
export class PostDetailComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit {
  @Input({ required: true }) slug!: string;

  post    = signal<PostDto | null>(null);
  loading = signal(true);

  private postsService    = inject(PostsService);
  private destroyRef      = inject(DestroyRef);   // for takeUntilDestroyed

  // ── ngOnInit ─── after first ngOnChanges, inputs are set ─────────────────
  ngOnInit(): void {
    // ✅ Correct: load data here (inputs are available)
    this.loadPost(this.slug);
    // ❌ Wrong: do NOT put data loading in constructor
    //    (inputs not yet set in constructor)
  }

  // ── ngOnChanges ─── runs BEFORE ngOnInit and on every input change ────────
  ngOnChanges(changes: SimpleChanges): void {
    // React to @Input changes after the component is initialised
    if (changes['slug'] && !changes['slug'].firstChange) {
      // Reload when slug input changes (navigating between posts)
      this.loadPost(changes['slug'].currentValue);
    }
  }

  // ── ngAfterViewInit ─── after template is fully rendered ──────────────────
  ngAfterViewInit(): void {
    // Access @ViewChild references here (not in ngOnInit — not yet rendered)
    // Focus an input, initialise a third-party chart library, etc.
  }

  // ── ngOnDestroy ─── cleanup before component is removed ──────────────────
  ngOnDestroy(): void {
    // Manual approach: cancel subscriptions, clear timers
    // Modern approach: takeUntilDestroyed() (shown below)
  }

  private loadPost(slug: string): void {
    this.loading.set(true);

    // ── Modern: takeUntilDestroyed — auto-unsubscribes on destroy ─────────
    this.postsService.getBySlug(slug)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe({
        next:  post => { this.post.set(post); this.loading.set(false); },
        error: ()   => { this.loading.set(false); },
      });
  }
}
Note: The lifecycle hook execution order is: constructorngOnChanges (first, with input values) → ngOnInitngAfterContentInitngAfterViewInit → (update cycles: ngOnChangesngDoCheck) → ngOnDestroy. The key practical rules: put data loading in ngOnInit (not constructor), react to input changes in ngOnChanges (check !firstChange to skip the initial call), access @ViewChild references in ngAfterViewInit (not before), and clean up in ngOnDestroy.
Tip: Use takeUntilDestroyed() from @angular/core/rxjs-interop (Angular 16+) as the standard pattern for managing RxJS subscriptions in components. It automatically unsubscribes when the component is destroyed, without needing a Subject-based destroy pattern (private destroy$ = new Subject()) or manual ngOnDestroy implementation. Inject DestroyRef in the constructor and pass it to takeUntilDestroyed(this.destroyRef) for the most explicit and flexible usage.
Warning: Every RxJS subscription that does not complete on its own must be unsubscribed. Subscriptions from HttpClient complete after the HTTP response — no cleanup needed. Subscriptions to Router.events, ActivatedRoute.params, FormControl.valueChanges, and any interval() or timer() do NOT complete automatically — they must be unsubscribed in ngOnDestroy or via takeUntilDestroyed(). Unmanaged subscriptions on destroyed components cause memory leaks and “ExpressionChangedAfterItHasBeenCheckedError” errors.

Lifecycle Hook Checklist

Hook When Use For
constructor Component instantiation DI injection only; no inputs yet
ngOnChanges Before ngOnInit + on each input change React to @Input changes
ngOnInit Once after first ngOnChanges Initial data loading, setup
ngAfterViewInit After template fully rendered Access @ViewChild, third-party libs
ngOnDestroy Before component removed from DOM Unsubscribe, clear timers

Common Mistakes

Mistake 1 — Loading data in constructor instead of ngOnInit (inputs not set yet)

❌ Wrong — constructor(private svc: PostsService) { svc.getBySlug(this.slug); } — this.slug is undefined.

✅ Correct — load data in ngOnInit() where all @Input properties are guaranteed to be set.

Mistake 2 — Not unsubscribing from long-lived observables (memory leak)

❌ Wrong — subscribing to router.events without cleanup; subscription persists after component destroyed.

✅ Correct — add .pipe(takeUntilDestroyed(this.destroyRef)) to all non-completing subscriptions.

🧠 Test Yourself

A component navigates between posts by changing the slug @Input. Where should the data reload logic go — in ngOnInit or ngOnChanges?