Skip to main content

Overview

Lifecycle hooks provide visibility into key moments in a component or directive’s lifecycle, from creation to destruction. Angular calls these hook methods in a specific sequence.

Lifecycle Sequence

  1. Constructor - Class instantiation (not a hook)
  2. OnChanges - Input property changes
  3. OnInit - Initialization after first change detection
  4. DoCheck - Custom change detection
  5. AfterContentInit - Content projection initialized (once)
  6. AfterContentChecked - Content projection checked
  7. AfterViewInit - View initialized (once)
  8. AfterViewChecked - View checked
  9. OnDestroy - Cleanup before destruction

OnChanges

Interface

interface OnChanges {
  ngOnChanges(changes: SimpleChanges): void;
}

Description

Called when any data-bound input property changes. Receives a SimpleChanges object containing current and previous values.

Parameters

changes
SimpleChanges
Object containing changed properties with their current and previous values.
{
  propertyName: {
    currentValue: any,
    previousValue: any,
    firstChange: boolean
  }
}

Example

user-profile.component.ts
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  standalone: true,
  template: `
    <div>
      <h3>{{userName}}</h3>
      <p>Changes: {{changeCount}}</p>
    </div>
  `
})
export class UserProfileComponent implements OnChanges {
  @Input() userName: string = '';
  @Input() userId: number = 0;
  changeCount = 0;

  ngOnChanges(changes: SimpleChanges) {
    this.changeCount++;
    
    if (changes['userName']) {
      console.log('userName changed:', {
        previous: changes['userName'].previousValue,
        current: changes['userName'].currentValue,
        first: changes['userName'].firstChange
      });
    }
    
    if (changes['userId'] && !changes['userId'].firstChange) {
      // Reload user data when ID changes (but not on first load)
      this.loadUserData(changes['userId'].currentValue);
    }
  }
  
  private loadUserData(id: number) {
    console.log('Loading data for user:', id);
  }
}
ngOnChanges is called before ngOnInit and whenever input properties change. It’s not called for internal state changes.

OnInit

Interface

interface OnInit {
  ngOnInit(): void;
}

Description

Called once after the first ngOnChanges. Used for component initialization logic.

Example

data-list.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-data-list',
  standalone: true,
  template: `
    <ul>
      <li *ngFor="let item of items">{{item}}</li>
    </ul>
  `
})
export class DataListComponent implements OnInit {
  items: string[] = [];

  constructor(private dataService: DataService) {
    // Constructor should be lightweight
    console.log('Constructor called');
  }

  ngOnInit() {
    // Initialization logic goes here
    console.log('ngOnInit called');
    this.loadData();
  }

  private loadData() {
    this.dataService.getData().subscribe(data => {
      this.items = data;
    });
  }
}
Use ngOnInit for initialization logic that requires input properties or depends on services. Keep constructors lightweight.

DoCheck

Interface

interface DoCheck {
  ngDoCheck(): void;
}

Description

Called during every change detection cycle. Use for custom change detection logic.

Example

custom-check.component.ts
import { Component, Input, DoCheck } from '@angular/core';

@Component({
  selector: 'app-custom-check',
  standalone: true,
  template: `
    <div>
      <p>Items: {{items.length}}</p>
      <p>Last check: {{lastCheck}}</p>
    </div>
  `
})
export class CustomCheckComponent implements DoCheck {
  @Input() items: string[] = [];
  lastCheck: Date = new Date();
  private previousLength = 0;

  ngDoCheck() {
    this.lastCheck = new Date();
    
    // Custom check for array length changes
    if (this.items.length !== this.previousLength) {
      console.log('Array length changed:', this.previousLength, '->', this.items.length);
      this.previousLength = this.items.length;
    }
  }
}
ngDoCheck is called very frequently. Keep its logic lightweight to avoid performance issues.

AfterContentInit

Interface

interface AfterContentInit {
  ngAfterContentInit(): void;
}

Description

Called once after Angular projects external content into the component’s view (content children).

Example

tab-container.component.ts
import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { TabComponent } from './tab.component';

@Component({
  selector: 'app-tab-container',
  standalone: true,
  template: `
    <div class="tab-container">
      <ng-content></ng-content>
    </div>
  `
})
export class TabContainerComponent implements AfterContentInit {
  @ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;

  ngAfterContentInit() {
    console.log('Content initialized');
    console.log(`Found ${this.tabs.length} tabs`);
    
    // Activate the first tab
    if (this.tabs.length > 0) {
      this.tabs.first.active = true;
    }
  }
}

AfterContentChecked

Interface

interface AfterContentChecked {
  ngAfterContentChecked(): void;
}

Description

Called after Angular checks the content projected into the component.

Example

content-wrapper.component.ts
import { Component, ContentChild, AfterContentChecked, ElementRef } from '@angular/core';

@Component({
  selector: 'app-content-wrapper',
  standalone: true,
  template: `
    <div class="wrapper">
      <ng-content></ng-content>
      <p>Content height: {{contentHeight}}px</p>
    </div>
  `
})
export class ContentWrapperComponent implements AfterContentChecked {
  @ContentChild('content') content?: ElementRef;
  contentHeight = 0;

  ngAfterContentChecked() {
    if (this.content) {
      this.contentHeight = this.content.nativeElement.offsetHeight;
    }
  }
}

AfterViewInit

Interface

interface AfterViewInit {
  ngAfterViewInit(): void;
}

Description

Called once after Angular initializes the component’s views and child views.

Example

chart.component.ts
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

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

  ngAfterViewInit() {
    console.log('View initialized');
    this.initializeChart();
  }

  private initializeChart() {
    const ctx = this.canvas.nativeElement.getContext('2d');
    if (ctx) {
      // Initialize chart library
      console.log('Canvas ready:', this.canvas.nativeElement.width);
    }
  }
}
View children are guaranteed to be initialized in ngAfterViewInit. Don’t try to access them in ngOnInit.

AfterViewChecked

Interface

interface AfterViewChecked {
  ngAfterViewChecked(): void;
}

Description

Called after Angular checks the component’s views and child views.

Example

scroll-monitor.component.ts
import { Component, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';

@Component({
  selector: 'app-scroll-monitor',
  standalone: true,
  template: `
    <div #scrollContainer class="container">
      <div class="content">{{content}}</div>
    </div>
    <p>Scroll position: {{scrollPosition}}</p>
  `
})
export class ScrollMonitorComponent implements AfterViewChecked {
  @ViewChild('scrollContainer') container!: ElementRef;
  scrollPosition = 0;
  content = 'Some content...';

  ngAfterViewChecked() {
    if (this.container) {
      this.scrollPosition = this.container.nativeElement.scrollTop;
    }
  }
}
Be cautious with ngAfterViewChecked - it’s called very frequently. Avoid expensive operations or state changes that trigger additional change detection.

OnDestroy

Interface

interface OnDestroy {
  ngOnDestroy(): void;
}

Description

Called just before Angular destroys the component or directive. Used for cleanup logic.

Example

timer.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { interval, Subscription } from 'rxjs';

@Component({
  selector: 'app-timer',
  standalone: true,
  template: `
    <div>
      <p>Elapsed: {{elapsed}} seconds</p>
    </div>
  `
})
export class TimerComponent implements OnInit, OnDestroy {
  elapsed = 0;
  private subscription?: Subscription;

  ngOnInit() {
    console.log('Timer started');
    this.subscription = interval(1000).subscribe(() => {
      this.elapsed++;
    });
  }

  ngOnDestroy() {
    console.log('Timer stopped');
    // Clean up subscription to prevent memory leaks
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }
}
Always clean up in ngOnDestroy:
  • Unsubscribe from Observables
  • Clear intervals and timeouts
  • Detach event listeners
  • Cancel pending HTTP requests

Complete Lifecycle Example

full-lifecycle.component.ts
import {
  Component,
  Input,
  OnChanges,
  OnInit,
  DoCheck,
  AfterContentInit,
  AfterContentChecked,
  AfterViewInit,
  AfterViewChecked,
  OnDestroy,
  SimpleChanges
} from '@angular/core';

@Component({
  selector: 'app-full-lifecycle',
  standalone: true,
  template: `
    <div>
      <h3>{{title}}</h3>
      <ng-content></ng-content>
    </div>
  `
})
export class FullLifecycleComponent implements
  OnChanges,
  OnInit,
  DoCheck,
  AfterContentInit,
  AfterContentChecked,
  AfterViewInit,
  AfterViewChecked,
  OnDestroy {
  
  @Input() title = 'Component';
  
  constructor() {
    console.log('1. Constructor');
  }

  ngOnChanges(changes: SimpleChanges) {
    console.log('2. ngOnChanges', changes);
  }

  ngOnInit() {
    console.log('3. ngOnInit');
  }

  ngDoCheck() {
    console.log('4. ngDoCheck');
  }

  ngAfterContentInit() {
    console.log('5. ngAfterContentInit');
  }

  ngAfterContentChecked() {
    console.log('6. ngAfterContentChecked');
  }

  ngAfterViewInit() {
    console.log('7. ngAfterViewInit');
  }

  ngAfterViewChecked() {
    console.log('8. ngAfterViewChecked');
  }

  ngOnDestroy() {
    console.log('9. ngOnDestroy');
  }
}

Hook Call Frequency

HookFrequencyWhen to Use
ngOnChangesOn input changesReact to input changes
ngOnInitOnceInitialization logic
ngDoCheckEvery CD cycleCustom change detection
ngAfterContentInitOnceAccess content children
ngAfterContentCheckedEvery CD cycleMonitor content changes
ngAfterViewInitOnceAccess view children, DOM manipulation
ngAfterViewCheckedEvery CD cycleMonitor view changes
ngOnDestroyOnceCleanup

Best Practices

  • Use ngOnInit for most initialization logic
  • Keep ngDoCheck, ngAfterContentChecked, and ngAfterViewChecked lightweight
  • Always implement ngOnDestroy for components with subscriptions
  • Use ngAfterViewInit to access child components and DOM elements
  • Prefer ngOnChanges over getters/setters for input change detection
  • Don’t modify component state in ngAfterViewChecked (causes ExpressionChangedAfterItHasBeenCheckedError)
  • Avoid heavy computations in frequently called hooks
  • Don’t forget to unsubscribe from Observables in ngOnDestroy
  • Be careful with async operations in lifecycle hooks

Signal-Based Alternative

With Angular signals, many lifecycle hooks can be replaced:
signal-component.ts
import { Component, input, effect, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-signal-component',
  standalone: true,
  template: `<p>{{name()}}: {{count()}}</p>`
})
export class SignalComponent implements OnDestroy {
  name = input('Item');
  count = input(0);
  
  // Automatically runs when signals change
  private logEffect = effect(() => {
    console.log(`${this.name()} count is ${this.count()}`);
  });
  
  ngOnDestroy() {
    // Effects are automatically cleaned up
  }
}

See Also

Lifecycle Guide

Component lifecycle guide

Change Detection

How change detection works

Queries

Query child components

Signals

Modern reactive patterns