Angular Lifecycle Events: A Beginner's Guide
Web Development Angular

Angular Lifecycle Events: A Beginner's Guide

Mishel Shaji
Mishel Shaji

When I first started building Angular applications, one of the biggest hurdles I faced wasn't writing HTML templates or creating services—it was understanding when things actually happen inside a component.

Have you ever tried fetching data from an API inside a component, only to get an undefined error on an input property? Or tried to manipulate a DOM element before Angular finished rendering it? If so, you have bumped headfirst into the Angular Component Lifecycle.

Understanding lifecycle events is like learning the rhythm of Angular. Once you know which hook runs when, you stop fighting the framework and start building smooth, bug-free applications.

What Is a Component Lifecycle?

Think of an Angular component like a living organism. It gets born (created), it grows and updates when new information comes in (change detection), and eventually, it dies (gets removed from the screen).

 [Born]                  [Lives & Updates]               [Destroyed]
Creation -------------> Data Changes / Rendering ------------> Cleanup
(constructor,           (ngOnChanges, ngOnInit,              (ngOnDestroy)
 ngOnInit)              ngAfterViewInit, etc.)

Angular gives us special built-in methods called Lifecycle Hooks. These hooks allow us to step in at specific moments during a component's life and run our own code.

The Execution Order at a Glance

Before diving into each hook, here is the exact chronological order Angular follows during a component's life cycle (starting with JavaScript instantiation):

Order Method / Hook Frequency Primary Purpose
1 constructor() Once Class instantiation & dependency injection.
2 ngOnChanges() Whenever @Input() changes Respond to updated input bindings.
3 ngOnInit() Once Initialize component logic after inputs are ready.
4 ngDoCheck() Every change detection run Custom change detection for complex state.
5 ngAfterContentInit() Once After projected content (<ng-content>) is loaded.
6 ngAfterContentChecked() Every change detection run After projected content has been checked.
7 ngAfterViewInit() Once After component view & child views are fully loaded.
8 ngAfterViewChecked() Every change detection run After component view & child views are checked.
9 ngOnDestroy() Once Cleanup logic right before the component dies.

Deep Dive into Every Lifecycle Hook

Let me walk you through how each lifecycle hook works in code, why it exists, and when you should use it.

1. The Constructor (constructor)

While not technically an Angular lifecycle hook (it's a standard JavaScript class constructor), it is step 1 where everything begins.

  • When it runs: First, when JavaScript creates the component instance.
  • Best for: Injecting services via Dependency Injection (DI).
  • Avoid: Fetching API data or reading @Input() values here. Input bindings are not available inside the constructor yet.
import { Component, Input } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-card',
  template: `<h2>{{ userTitle }}</h2>`
})
export class UserCardComponent {
  @Input() userId!: string;
  userTitle = '';

  // Good: Inject services here
  constructor(private userService: UserService) {
    // Bad: this.userId is undefined here!
    // console.log(this.userId); 
  }
}

2. ngOnChanges

ngOnChanges is the first official Angular lifecycle hook to run if your component receives @Input() properties.

  • When it runs: Before ngOnInit(), and every single time an @Input() property changes.
  • Parameter: It provides a SimpleChanges object containing the previous value, current value, and whether it's the first change.
  • Best for: Reacting instantly whenever a parent component updates a value passed down to this component.
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-user-card',
  template: `<p>User ID: {{ userId }}</p>`
})
export class UserCardComponent implements OnChanges {
  @Input() userId!: string;

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['userId']) {
      const prev = changes['userId'].previousValue;
      const current = changes['userId'].currentValue;
      console.log(`userId changed from ${prev} to ${current}`);
    }
  }
}

3. ngOnInit

ngOnInit is the most widely used lifecycle hook in Angular development.

  • When it runs: Exactly once, right after the first ngOnChanges run. At this point, all @Input() properties are fully initialized and ready to use.
  • Best for: Fetching initial data from backend APIs, initializing complex component properties, and setting up component state.
import { Component, OnInit, Input } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-profile',
  template: `<div *ngIf="userData">{{ userData.name }}</div>`
})
export class UserProfileComponent implements OnInit {
  @Input() userId!: string;
  userData: any;

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    // Perfect place to fetch data!
    this.userService.getUser(this.userId).subscribe(data => {
      this.userData = data;
    });
  }
}

4. ngDoCheck

ngDoCheck gives you access to Angular's internal change detection engine.

  • When it runs: On every single change detection run—whenever a button is clicked, a timer fires, or an HTTP request completes.
  • Best for: Custom change detection when Angular's automatic change detection misses something (e.g., deep mutations inside complex objects or arrays).
  • Warning: Code placed here executes constantly. Keep logic minimal to avoid severe performance issues.
import { Component, DoCheck, Input } from '@angular/core';

@Component({
  selector: 'app-item-list',
  template: `<p>Total items: {{ items.length }}</p>`
})
export class ItemListComponent implements DoCheck {
  @Input() items: string[] = [];
  private previousLength = 0;

  ngDoCheck(): void {
    // Detect array mutations that change length without changing reference
    if (this.items.length !== this.previousLength) {
      console.log('Array length changed!');
      this.previousLength = this.items.length;
    }
  }
}

5 & 6. ngAfterContentInit and ngAfterContentChecked

These hooks deal with Content Projection (using <ng-content> to insert external HTML or components inside your component).

  • ngAfterContentInit: Fires once after Angular projects external content into the component. Use it when you need to access elements marked with @ContentChild or @ContentChildren.
  • ngAfterContentChecked: Fires after every change detection cycle that checks projected content.
import { Component, AfterContentInit, ContentChild, ElementRef } from '@angular/core';

@Component({
  selector: 'app-card',
  template: `
    <div class="card-header">
      <ng-content select="header"></ng-content>
    </div>
  `
})
export class CardComponent implements AfterContentInit {
  @ContentChild('headerText') headerEl!: ElementRef;

  ngAfterContentInit(): void {
    // Access projected header elements safely
    console.log('Projected header content:', this.headerEl.nativeElement.innerText);
  }
}

7 & 8. ngAfterViewInit and ngAfterViewChecked

These hooks fire after Angular has finished rendering the component's HTML template and all of its child components.

  • ngAfterViewInit: Fires once after the template view and child views are fully initialized. This is the first moment you can safely access DOM elements using @ViewChild or @ViewChildren.
  • ngAfterViewChecked: Fires after every change detection check of the view.
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';

@Component({
  selector: 'app-custom-input',
  template: `<input #searchBox type="text" placeholder="Search..." />`
})
export class CustomInputComponent implements AfterViewInit {
  @ViewChild('searchBox') searchInput!: ElementRef;

  ngAfterViewInit(): void {
    // Focus the input element as soon as the view is ready
    this.searchInput.nativeElement.focus();
  }
}
Developer Pro-Tip: Do not update component state variables inside ngAfterViewInit without wrapping them in setTimeout() or triggering a manual change detection, or you will trigger Angular's notorious ExpressionChangedAfterItHasBeenCheckedError.

9. ngOnDestroy (and Modern DestroyRef)

ngOnDestroy is your cleanup worker.

  • When it runs: Right before Angular destroys the component and removes its DOM elements.
  • Best for: Unsubscribing from RxJS Observables, clearing setInterval / setTimeout timers, and detaching event listeners to avoid memory leaks.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';

@Component({
  selector: 'app-timer',
  template: `<p>Timer running...</p>`
})
export class TimerComponent implements OnInit, OnDestroy {
  private timerSub!: Subscription;

  ngOnInit(): void {
    this.timerSub = interval(1000).subscribe(val => console.log(val));
  }

  ngOnDestroy(): void {
    // Always clean up subscriptions to prevent memory leaks!
    this.timerSub.unsubscribe();
    console.log('Component destroyed, subscription cleaned up.');
  }
}

Modern Alternative: DestroyRef and takeUntilDestroyed

In modern Angular, you can avoid writing boilerplate ngOnDestroy methods by using DestroyRef or the RxJS operator takeUntilDestroyed:

import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';

@Component({
  selector: 'app-timer',
  template: `<p>Modern Clean Timer</p>`
})
export class ModernTimerComponent {
  constructor() {
    // Automatically unsubscribes when the component is destroyed!
    interval(1000)
      .pipe(takeUntilDestroyed())
      .subscribe(val => console.log(val));
  }
}

Modern SSR-Safe Hooks: afterNextRender and afterRender

With modern Angular support for Server-Side Rendering (SSR), traditional hooks like ngAfterViewInit sometimes cause issues because they execute on both the server and the browser.

Angular introduced two modern lifecycle functions designed specifically for safe browser DOM operations:

  1. afterNextRender: Runs once in the browser after the next change detection cycle. Ideal for initializing third-party browser-only libraries (e.g., Chart.js, Leaflet maps, D3).
  2. afterRender: Runs after every render cycle in the browser. Useful for re-synchronizing custom canvas elements or third-party DOM components.
import { Component, ElementRef, ViewChild, afterNextRender } from '@angular/core';

@Component({
  selector: 'app-chart',
  template: `<canvas #chartCanvas></canvas>`
})
export class ChartComponent {
  @ViewChild('chartCanvas') canvas!: ElementRef;

  constructor() {
    // Only runs in the browser, perfectly safe for SSR!
    afterNextRender(() => {
      // Initialize browser-only library here safely
      console.log('Canvas element is safe to manipulate:', this.canvas.nativeElement);
    });
  }
}

3 Common Mistakes Beginners Make

  1. Putting API calls inside constructor() instead of ngOnInit():
    • Problem: Inputs aren't ready yet, and testing becomes harder.
    • Fix: Keep constructors lean (for dependency injection only) and move fetch calls to ngOnInit().
  2. Forgetting to unsubscribe in ngOnDestroy():
    • Problem: Long-running RxJS subscriptions remain active in memory long after the user leaves the page.
    • Fix: Use takeUntilDestroyed() or explicitly unsubscribe in ngOnDestroy().
  3. Accessing @ViewChild before ngAfterViewInit():
    • Problem: Trying to read @ViewChild inside ngOnInit() returns undefined because the view hasn't finished rendering yet.
    • Fix: Always wait until ngAfterViewInit() (or afterNextRender()) to manipulate view elements.

Mastering these lifecycle events gives you predictable control over your Angular apps. Start simple: rely on ngOnInit for initialization, ngOnChanges for input tracking, ngAfterViewInit for DOM access, and ngOnDestroy for cleanup. As you build more complex features, the remaining hooks will fall naturally into place.