Component Lifecycle Hook

By Syncfusion®, Inc. · Last updated

The stages of a component's life and the hooks Angular calls at each one, from ngOnChanges and ngOnInit to the content and view hooks and ngOnDestroy, with best practices.

Introduction to Component Lifecycle

The component lifecycle is the sequence of stages a component goes through: Angular creates it, sets its inputs, renders its view, checks it for changes, and finally destroys it.

At each stage, Angular calls a lifecycle hook: a method with a fixed name, such as ngOnInit or ngOnDestroy. If your component class defines that method, Angular runs your code at that moment.

Real-World Example

A live stock-price widget must do different work at different moments. When it first appears, it loads the current prices. When the user switches to another market, it reloads them. Once its template is on the screen, it places the cursor in the search box.

When the user leaves the page, the widget must close its live connection and stop its refresh timer. Otherwise the app keeps sending network requests for a widget nobody can see. Lifecycle hooks give you a method for each of these moments.

Angular Lifecycle Hooks Overview

ngOnChangesngOnInitngDoCheckAfterContentAfterViewngOnDestroyEvery change: ngOnChanges (if inputs changed), ngDoCheck, the Checked hookscreatedremoved

Creation runs left to right once. Each change detection run repeats the check hooks.

Angular calls the hooks in this order:

OrderHookWhen Angular calls itRuns
1ngOnChangesAn input value is set or changesBefore ngOnInit, then on every input change
2ngOnInitAfter the first inputs are setOnce
3ngDoCheckDuring every change detection runMany times
4ngAfterContentInitAfter projected content is placedOnce
5ngAfterContentCheckedAfter projected content is checkedMany times
6ngAfterViewInitAfter the component's view and child views are createdOnce
7ngAfterViewCheckedAfter the view is checkedMany times
8ngOnDestroyJust before Angular removes the componentOnce

Each hook has a matching TypeScript interface, such as OnInit or OnDestroy. Writing implements OnInit is optional, but it makes the compiler check the method name.

import { Component, OnDestroy, OnInit } from '@angular/core';

@Component({ selector: 'app-clock', template: `...` })
export class Clock implements OnInit, OnDestroy {
  ngOnInit() { /* start */ }
  ngOnDestroy() { /* clean up */ }
}

Most components need only two hooks: ngOnInit to start work and ngOnDestroy to clean up. Use the other hooks when you have a specific reason.

The next block logs every hook as it runs. Click the buttons and read the log.

Every lifecycle hook, logged in the order Angular calls it

import {
  AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, DoCheck,
  Input, OnChanges, OnDestroy, OnInit, SimpleChanges, signal,
} from '@angular/core';

const log: string[] = [];

@Component({
  selector: 'app-tracker',
  template: `<p>Tracker shows: {{ value }} <ng-content /></p>`,
})
export class Tracker implements OnChanges, OnInit, DoCheck, AfterContentInit,
  AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
  @Input() value = 0;
  checks = 0;

  ngOnChanges(changes: SimpleChanges) { log.push('ngOnChanges: value = ' + changes['value'].currentValue); }
  ngOnInit() { log.push('ngOnInit'); }
  ngDoCheck() { this.checks++; }
  ngAfterContentInit() { log.push('ngAfterContentInit'); }
  ngAfterContentChecked() {}
  ngAfterViewInit() { log.push('ngAfterViewInit'); }
  ngAfterViewChecked() {}
  ngOnDestroy() { log.push('ngOnDestroy'); }
}

@Component({
  selector: 'app-root',
  imports: [Tracker],
  template: `
    <button (click)="value = value + 1; refresh()">Change input</button>
    <button (click)="show = !show; refresh()">{{ show ? 'Destroy' : 'Create' }} tracker</button>
    @if (show) {
      <app-tracker [value]="value">(projected text)</app-tracker>
    }
    <ol>
      @for (line of lines(); track $index) { <li>{{ line }}</li> }
    </ol>
  `,
})
export class App implements AfterViewInit {
  value = 1;
  show = true;
  lines = signal<string[]>([]);

  // The hooks run while Angular updates the page, so the log is copied
  // into a signal just after that update finishes.
  ngAfterViewInit() {
    this.refresh();
  }

  refresh() {
    setTimeout(() => this.lines.set([...log]));
  }
}

The first run logs ngOnChanges, ngOnInit, ngAfterContentInit and ngAfterViewInit, in that order. Change input adds another ngOnChanges. Destroy tracker adds ngOnDestroy.

ngOnChanges

ngOnChanges runs whenever Angular sets or changes an input of the component. It runs once before ngOnInit, and again each time the parent passes a new value.

Parent sets [name]"Asha" → "Ravi"Angularsees a new inputngOnChanges()gets the changeschanges.nameAsha → RavingOnChanges runs every time an @Input() gets a new value.

ngOnChanges receives the old and the new value of each changed input.

It receives a SimpleChanges object. Each key is an input name, and each value has three properties:

  • currentValue: the new value.
  • previousValue: the old value (undefined on the first call).
  • firstChange: true on the first call.

ngOnChanges reacts to a new input value

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-price-tag',
  template: `<p>Price: ₹{{ price }} {{ trend }}</p>`,
})
export class PriceTag implements OnChanges {
  @Input() price = 0;
  trend = '';

  ngOnChanges(changes: SimpleChanges) {
    const change = changes['price'];
    if (change.firstChange) {
      this.trend = '(new)';
    } else {
      this.trend = change.currentValue > change.previousValue ? '▲ up' : '▼ down';
    }
  }
}

@Component({
  selector: 'app-root',
  imports: [PriceTag],
  template: `
    <app-price-tag [price]="price" />
    <button (click)="price = price + 50">Raise price</button>
    <button (click)="price = price - 50">Lower price</button>
  `,
})
export class App {
  price = 1000;
}

ngOnChanges runs only when the input receives a new reference. Changing a property inside an object input, such as user.name = 'Ravi', does not trigger it. Pass a new object instead: user = { ...user, name: 'Ravi' }.

ngOnInit

ngOnInit runs once, after Angular sets the first input values. It is the standard place to start the component's work: load data, read inputs, or set up initial state.

Use ngOnInit instead of the constructor for this work. In the constructor, the inputs are not set yet.

ngOnInit loads data after the inputs are ready

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-order-summary',
  template: `
    <p>Constructor saw customerId: {{ seenInConstructor }}</p>
    <p>ngOnInit saw customerId: {{ customerId }}</p>
    <p>{{ status }}</p>
  `,
})
export class OrderSummary implements OnInit {
  @Input() customerId = 0;
  seenInConstructor = 0;
  status = '';

  constructor() {
    this.seenInConstructor = this.customerId;
  }

  ngOnInit() {
    this.status = 'Loading orders for customer ' + this.customerId + '…';
  }
}

@Component({
  selector: 'app-root',
  imports: [OrderSummary],
  template: `<app-order-summary [customerId]="42" />`,
})
export class App {}

The constructor sees the default value 0. ngOnInit sees 42, the value the parent passed.

ngDoCheck

ngDoCheck runs during every change detection run, after ngOnChanges and ngOnInit. Angular runs change detection after events, timers and HTTP responses, so this hook can run very often.

Use it only to detect changes that Angular does not track by itself, such as a property changed inside an object input. Keep the code very fast.

ngDoCheck detects a change inside an object

import { Component, DoCheck, Input } from '@angular/core';

interface Cart {
  items: string[];
}

@Component({
  selector: 'app-cart-badge',
  template: `<p>🛒 {{ itemCount }} item(s)</p>`,
})
export class CartBadge implements DoCheck {
  @Input() cart: Cart = { items: [] };
  itemCount = 0;

  ngDoCheck() {
    if (this.cart.items.length !== this.itemCount) {
      this.itemCount = this.cart.items.length;
    }
  }
}

@Component({
  selector: 'app-root',
  imports: [CartBadge],
  template: `
    <app-cart-badge [cart]="cart" />
    <button (click)="cart.items.push('Item ' + (cart.items.length + 1))">Add item</button>
  `,
})
export class App {
  cart = { items: ['Item 1'] };
}

The button changes the array inside the same cart object, so ngOnChanges does not run. ngDoCheck notices the new length.

ngDoCheck, ngAfterContentChecked and ngAfterViewChecked run on every change detection cycle. Slow code in these hooks slows down the whole app. Prefer passing a new object to the input, so ngOnChanges can do the work instead.

ngAfterContentInit

Content projection lets a parent place its own HTML inside a child component's tag. The child marks where that HTML goes with <ng-content />.

Content (from the parent)View (the own template)<app-card> ... </app-card>HTML between the tags<ng-content />where it is shownngAfterContentInitcontent is readytemplate: ...written in the componentchild componentscreated from the templatengAfterViewInitview is ready

Content comes from the parent. The view is the component's own template.

ngAfterContentInit runs once, after Angular has placed the projected content inside the child. Use it when the component must read or count that content.

ngAfterContentInit reads the projected content

import { AfterContentInit, Component, ContentChildren, Directive, QueryList } from '@angular/core';

@Directive({ selector: '[appTab]' })
export class TabMarker {}

@Component({
  selector: 'app-tab-bar',
  template: `<div class="bar"><ng-content /></div><p>{{ info }}</p>`,
  styles: `.bar { display: flex; gap: 8px; }`,
})
export class TabBar implements AfterContentInit {
  @ContentChildren(TabMarker) tabs!: QueryList<TabMarker>;
  info = '';

  ngAfterContentInit() {
    this.info = 'This tab bar received ' + this.tabs.length + ' tabs.';
  }
}

@Component({
  selector: 'app-root',
  imports: [TabBar, TabMarker],
  template: `
    <app-tab-bar>
      <button appTab>Overview</button>
      <button appTab>Orders</button>
      <button appTab>Settings</button>
    </app-tab-bar>
  `,
})
export class App {}

@ContentChildren collects the projected elements that carry the appTab directive. They are available from ngAfterContentInit onward.

ngAfterContentChecked

ngAfterContentChecked runs after ngAfterContentInit, and again after every change detection check of the projected content.

Use it only when the component must react each time the projected content may have changed, for example to recount items. Like ngDoCheck, it runs often, so keep it fast.

ngAfterContentChecked() {
  // Runs after every check of the projected content.
  this.tabCount = this.tabs.length;
}

ngAfterViewInit

ngAfterViewInit runs once, after Angular has created the component's own view and the views of its children.

It is the first moment when elements from the template are available through @ViewChild. Use it to focus an input, measure an element, or start a third-party library that needs a DOM element, such as a chart.

ngAfterViewInit focuses an input and measures an element

import { AfterViewInit, Component, ElementRef, ViewChild, signal } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <input #search placeholder="The cursor starts here" />
    <div #banner class="banner">Spring sale: 20% off</div>
    <p>Banner width: {{ width() }}px</p>
  `,
  styles: `.banner { display: inline-block; padding: 8px 16px; background: #fde68a; }`,
})
export class App implements AfterViewInit {
  @ViewChild('search') search!: ElementRef<HTMLInputElement>;
  @ViewChild('banner') banner!: ElementRef<HTMLDivElement>;
  width = signal(0);

  ngAfterViewInit() {
    this.search.nativeElement.focus();
    this.width.set(this.banner.nativeElement.offsetWidth);
  }
}

width is a signal. Setting a signal tells Angular to check the view again. If width were a plain property, changing it here would make Angular report ExpressionChangedAfterItHasBeenCheckedError in development mode, because the view has already been checked.

PointngAfterContentInitngAfterViewInit
Waits forProjected content (<ng-content>)The component's own template and child views
Query decorator@ContentChildren, @ContentChild@ViewChildren, @ViewChild
Typical useCount or configure projected itemsFocus, measure, start DOM libraries

ngAfterViewChecked

ngAfterViewChecked runs after ngAfterViewInit, and again after every check of the component's view.

Use it rarely, for example to keep a chat window scrolled to the newest message after the list re-renders. Never change displayed values here without care, because that starts another check.

ngAfterViewChecked() {
  // Keep the message list scrolled to the bottom.
  const list = this.messages.nativeElement;
  list.scrollTop = list.scrollHeight;
}

ngOnDestroy

ngOnDestroy runs once, just before Angular removes the component, for example when an @if becomes false or the user navigates away.

@if becomes falseor the route changesngOnDestroy()called onceClean upclearInterval, unsubscribeRemovedno code keeps runningWithout clean-up, timers keep running after the component is gone.

ngOnDestroy is the last chance to stop work that the component started.

Use it to clean up anything that would keep running after the component is gone:

  • Timers started with setInterval or setTimeout.
  • Subscriptions to observables, such as live data streams.
  • WebSocket connections and global event listeners on window or document.

ngOnDestroy stops the timer when the component is removed

import { Component, OnDestroy, OnInit, signal } from '@angular/core';

@Component({
  selector: 'app-session-timer',
  template: `<p>Session time: {{ seconds() }}s</p>`,
})
export class SessionTimer implements OnInit, OnDestroy {
  seconds = signal(0);
  private timerId?: ReturnType<typeof setInterval>;

  ngOnInit() {
    this.timerId = setInterval(() => this.seconds.update((s) => s + 1), 1000);
  }

  ngOnDestroy() {
    clearInterval(this.timerId);
    console.log('Timer stopped after ' + this.seconds() + 's');
  }
}

@Component({
  selector: 'app-root',
  imports: [SessionTimer],
  template: `
    <button (click)="active = !active">{{ active ? 'Sign out' : 'Sign in' }}</button>
    @if (active) { <app-session-timer /> }
  `,
})
export class App {
  active = true;
}

Wait a few seconds, then click Sign out. The console shows the message from ngOnDestroy. Without clearInterval, the timer would keep running in the background.

Lifecycle Best Practices

  • Keep the constructor light. Use it only for dependency injection. Start work in ngOnInit.
  • Clean up in ngOnDestroy. Every timer, subscription and global listener you start must be stopped.
  • Use ngOnChanges to react to inputs. Compare previousValue and currentValue instead of polling in ngDoCheck.
  • Pass new objects to inputs. Replace an object instead of changing it in place, so ngOnChanges runs.
  • Access template elements in ngAfterViewInit, not earlier. @ViewChild is not set before it.
  • Avoid the "Checked" hooks unless you have a measured need. They run on every change detection cycle.
  • Implement the interfaces, such as implements OnInit, so a misspelled hook name is reported by the compiler.

Common Mistakes

MistakeWhat you seeFix
Reading an input in the constructorThe default value, not the parent's valueRead inputs in ngOnInit or ngOnChanges
Misspelled hook, such as ngOnintThe method never runs, with no errorAdd implements OnInit so the compiler checks the name
Using @ViewChild in ngOnInitCannot read properties of undefinedUse it in ngAfterViewInit
Not clearing a timerThe timer keeps running after the component is goneCall clearInterval in ngOnDestroy
Changing a plain displayed property in ngAfterViewInitExpressionChangedAfterItHasBeenCheckedErrorStore the value in a signal, or set it earlier
Mutating an object inputngOnChanges does not runPass a new object: cart = { ...cart, items: [...] }

Check Yourself

Exercises

What to Remember

  • Angular calls lifecycle hooks in a fixed order: ngOnChanges, ngOnInit, ngDoCheck, the content hooks, the view hooks, and finally ngOnDestroy.
  • Start work in ngOnInit, react to inputs in ngOnChanges, and use template elements from ngAfterViewInit onward.
  • Clean up timers, subscriptions and listeners in ngOnDestroy.

Next: Template-Driven Forms, how to collect user input with forms built in the template.