Angular Lifecycle Hooks: OnInit, OnDestroy, OnChanges, DoCheck and More

Angular Tutorial: Beginner to Advanced · 6. Reusable components · 5 min read · Updated

Every component has a life: Angular creates it, shows it, updates it and finally destroys it. Lifecycle hooks are methods that Angular calls at those moments, so you can run your code at the right time.

What you will learn

  • The order of the hooks
  • What ngOnInit, ngOnDestroy, ngOnChanges and ngDoCheck are for
  • The after-view and after-content hooks
  • Modern signal-based alternatives

The order

For one component Angular calls, in this order:

  1. constructor (not a hook, but it runs first)
  2. ngOnChanges: when an input changes (before ngOnInit and on later changes)
  3. ngOnInit: once, after the first inputs are set
  4. ngDoCheck: on every change detection run
  5. ngAfterContentInit and ngAfterContentChecked: after projected content is ready
  6. ngAfterViewInit and ngAfterViewChecked: after the component's view is ready
  7. ngOnDestroy: just before the component is removed

See the hooks run

// src/app/lifecycle/lifecycle-logger.ts
import {
  AfterContentChecked,
  AfterContentInit,
  AfterViewChecked,
  AfterViewInit,
  Component,
  DoCheck,
  OnChanges,
  OnDestroy,
  OnInit,
  SimpleChanges,
  input,
  output,
} from '@angular/core';

@Component({
  selector: 'app-lifecycle-logger',
  template: `<p>Hello, {{ name() }}! <ng-content /></p>`,
})
export class LifecycleLogger
  implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy
{
  readonly name = input('friend');
  readonly log = output<string>();

  constructor() {
    this.log.emit('constructor');
  }

  ngOnChanges(changes: SimpleChanges): void {
    // changes['name'] has previousValue, currentValue and firstChange
    console.log('ngOnChanges', changes);
  }

  ngOnInit(): void {
    console.log('ngOnInit: name is', this.name());
  }

  ngDoCheck(): void {
    // Runs very often. Keep it cheap, and do not log here in real code.
  }

  ngAfterContentInit(): void {
    console.log('ngAfterContentInit');
  }

  ngAfterContentChecked(): void {}

  ngAfterViewInit(): void {
    console.log('ngAfterViewInit');
  }

  ngAfterViewChecked(): void {}

  ngOnDestroy(): void {
    console.log('ngOnDestroy');
  }
}

Each hook is an interface you implement. Implementing the interface is optional, but TypeScript then warns you about typos such as ngOnInt.

ngOnInit: start-up work

Use ngOnInit for work that needs the inputs, such as loading data for an id. The constructor is for simple setup like injecting services, because inputs are not available yet.

// src/app/lifecycle/user-card.ts
import { Component, OnInit, inject, input, signal } from '@angular/core';

@Component({
  selector: 'app-user-card',
  template: `<p>{{ status() }}</p>`,
})
export class UserCard implements OnInit {
  readonly userId = input.required<number>();
  protected readonly status = signal('not loaded');

  ngOnInit(): void {
    // In real code: this.api.load(this.userId()).subscribe(...)
    this.status.set(`Loaded user ${this.userId()}`);
  }
}

ngOnDestroy: clean up

Anything you start must be stopped: timers, event listeners on window, Observable subscriptions.

// src/app/lifecycle/ticker.ts
import { Component, OnDestroy, OnInit, signal } from '@angular/core';

@Component({
  selector: 'app-ticker',
  template: `<p>Ticks: {{ ticks() }}</p>`,
})
export class Ticker implements OnInit, OnDestroy {
  protected readonly ticks = signal(0);
  private timer?: ReturnType<typeof setInterval>;

  ngOnInit(): void {
    this.timer = setInterval(() => this.ticks.update((n) => n + 1), 1000);
  }

  ngOnDestroy(): void {
    clearInterval(this.timer); // without this the timer runs forever
  }
}

A modern shortcut is inject(DestroyRef).onDestroy(() => ...), which lets you register cleanup right where you start the work. takeUntilDestroyed() does the same for Observables.

ngOnChanges: react to input changes

ngOnChanges receives a SimpleChanges object with the previous and current value of each changed input:

// src/app/lifecycle/price-tag.ts
import { Component, OnChanges, SimpleChanges, input, signal } from '@angular/core';

@Component({
  selector: 'app-price-tag',
  template: `<p>{{ price() }} <span>{{ trend() }}</span></p>`,
})
export class PriceTag implements OnChanges {
  readonly price = input(0);
  protected readonly trend = signal('');

  ngOnChanges(changes: SimpleChanges): void {
    const change = changes['price'];
    if (change && !change.firstChange) {
      this.trend.set(change.currentValue > change.previousValue ? 'up' : 'down');
    }
  }
}

With signal inputs, you usually do not need ngOnChanges. Use computed() for derived values, or effect() for side effects, which re-run automatically when the input changes. ngOnChanges still fires for signal inputs (we checked this on Angular 22) and for older @Input() properties, so existing code keeps working.

ngDoCheck

ngDoCheck runs on every change detection pass. It exists for custom change detection, such as noticing a mutated object that Angular's default checks cannot see. Because it runs so often, keep its code very small. Most apps never need it.

After-content and after-view hooks

  • `ngAfterContentInit` runs once after content projected with <ng-content /> is ready. Use it with contentChild and contentChildren.
  • `ngAfterViewInit` runs once after the component's own template, and its child components, are rendered. Use it to measure or focus elements found with viewChild.
  • The ...Checked versions run after every check. Avoid heavy work in them.

Changing state that the template uses inside these hooks can trigger the ExpressionChangedAfterItHasBeenChecked error in development, which a later lesson explains.

Modern alternatives

New code often uses functions instead of hooks:

  • Run once in the browser after render: Modern way: afterNextRender(() => ...)
  • Run after every render: Modern way: afterEveryRender(() => ...)
  • Clean up: Modern way: inject(DestroyRef).onDestroy(...)
  • React to signal changes: Modern way: effect() and computed()

Hooks are not deprecated, and you will see them everywhere, so know both styles.

Put it together

// src/app/lifecycle/lifecycle-page.ts
import { Component, signal } from '@angular/core';
import { LifecycleLogger } from './lifecycle-logger';
import { PriceTag } from './price-tag';
import { Ticker } from './ticker';
import { UserCard } from './user-card';

@Component({
  selector: 'app-lifecycle-page',
  imports: [LifecycleLogger, PriceTag, Ticker, UserCard],
  template: `
    <button type="button" (click)="show.set(!show())">Toggle component</button>
    <button type="button" (click)="price.update(p => p + 5)">Raise price</button>

    @if (show()) {
      <app-lifecycle-logger [name]="'Ada'">projected</app-lifecycle-logger>
      <app-ticker />
    }
    <app-price-tag [price]="price()" />
    <app-user-card [userId]="7" />
  `,
})
export class LifecyclePage {
  protected readonly show = signal(true);
  protected readonly price = signal(10);
}
// src/app/app.ts
import { Component } from '@angular/core';
import { LifecyclePage } from './lifecycle/lifecycle-page';

@Component({
  imports: [LifecyclePage],
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
  <h1>Lifecycle</h1>
  <app-lifecycle-page />
</main>

Open the console and click "Toggle component" to watch ngOnDestroy and a new ngOnInit.

Try it yourself

  1. Log ngOnInit in two child components and see the order when both are on the page.
  2. Add a window resize listener in ngOnInit and remove it in ngOnDestroy.
  3. Replace the ticker's ngOnDestroy with inject(DestroyRef).onDestroy.

Quick check

  1. Which hook runs first after the constructor when an input is set?
  2. Where do you stop a timer?
  3. Why keep ngDoCheck small?

Next, see how components find and control elements in template reference variables and queries. Read the lifecycle guide.

Frequently asked questions

Constructor or ngOnInit?

Use the constructor for dependency injection and simple setup. Use ngOnInit when you need input values or want a clear start point for loading data.

Do I have to implement the interface?

No, Angular calls the method if it exists. The interface just gives you a compile-time check.