Back to CourseIntermediate4 min read
Angular /

Component Lifecycle Hooks

Learn the lifecycle hooks Angular still calls, which ones signals made unnecessary, and where each one genuinely belongs.

Bishal Biswas

Bishal Biswas

Self Learner

Published on: June 8, 2026
Last updated: September 7, 2026
Intermediate Topic

The hooks

Angular calls these methods at fixed points in a component’s life.

ngOnChanges          an input changed
ngOnInit             after the first ngOnChanges, once
ngDoCheck            on every change detection run
ngAfterContentInit   projected content is ready, once
ngAfterContentChecked  projected content checked
ngAfterViewInit      the view and child views are ready, once
ngAfterViewChecked   the view checked
ngOnDestroy          before the component is removed

They still work and are not deprecated. But signals made most of them unnecessary, and modern Angular code uses very few.

ngOnInit

The one you will still write occasionally.

export class UserList implements OnInit {
  private readonly service = inject(UserService);
  protected readonly users = signal<User[]>([]);

  ngOnInit(): void {
    this.service.getAll().subscribe(users => this.users.set(users));
  }
}

Why not the constructor? Historically, because inputs were not set yet. That reason has weakened with signal inputs, since you read those inside computed or effect anyway.

The modern alternative is usually a resource:

protected readonly users = httpResource<User[]>(() => '/api/users');

No hook at all, plus loading and error state for free.

ngOnDestroy

For cleanup. Still relevant, though there is a better option.

export class Poller implements OnDestroy {
  private readonly id = setInterval(() => this.check(), 1000);

  ngOnDestroy(): void {
    clearInterval(this.id);
  }
}

DestroyRef does the same without implementing an interface, and works in services and functions too:

export class Poller {
  constructor() {
    const id = setInterval(() => this.check(), 1000);
    inject(DestroyRef).onDestroy(() => clearInterval(id));
  }
}

Better still, an effect with cleanup:

constructor() {
  effect(onCleanup => {
    const id = setInterval(() => this.check(), this.interval());
    onCleanup(() => clearInterval(id));
  });
}

That handles both the interval changing and the component being destroyed.

For RxJS, takeUntilDestroyed removes the need for a hook entirely:

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

constructor() {
  this.service.updates$
    .pipe(takeUntilDestroyed())
    .subscribe(update => this.handle(update));
}

ngOnChanges

Runs whenever an input changes, with a record of what changed.

export class Chart implements OnChanges {
  @Input() data: number[] = [];
  @Input() colour = 'blue';

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['data']) {
      this.redraw();
    }
  }
}

With signal inputs this is unnecessary.

export class Chart {
  readonly data = input.required<number[]>();
  readonly colour = input('blue');

  constructor() {
    effect(() => this.redraw(this.data()));    // only when data changes
  }

  protected readonly summary = computed(() => this.data().length);
}

The effect depends only on what it reads, so there is no changes['data'] check. That string key was never type safe anyway.

Do not mix them. ngOnChanges does not fire reliably for signal inputs. Convert a component’s inputs together.

ngAfterViewInit

Ran when the view and child components existed. Used for DOM access and third party initialisation.

export class Chart implements AfterViewInit {
  @ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;

  ngAfterViewInit(): void {
    if (isPlatformBrowser(this.platformId)) {
      new ThirdPartyChart(this.canvas.nativeElement);
    }
  }
}

Replaced by afterNextRender, which does not run on the server and has cleanup built in:

export class Chart {
  private readonly canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');

  constructor() {
    afterNextRender(onCleanup => {
      const chart = new ThirdPartyChart(this.canvas().nativeElement);
      onCleanup(() => chart.destroy());
    });
  }
}

ngAfterContentInit

For projected content. Replaced by contentChild and contentChildren, which are reactive.

// old
@ContentChildren(Tab) tabs!: QueryList<Tab>;

ngAfterContentInit(): void {
  this.tabs.changes.subscribe(() => this.rebuild());
  this.rebuild();
}
// new
readonly tabs = contentChildren(Tab);

protected readonly tabLabels = computed(() => this.tabs().map(t => t.label()));

ngDoCheck and the Checked hooks

ngDoCheck, ngAfterContentChecked and ngAfterViewChecked run on every change detection pass, which can be hundreds of times a second.

They exist for custom change detection, such as noticing a mutation inside an object that Angular cannot see. That need mostly disappears with signals, where you replace values rather than mutate them.

Avoid them. Anything expensive in one is a guaranteed performance problem, and it is very easy to cause an infinite loop by changing state inside one.

The order

For a parent with a child, on first render:

parent  ngOnChanges
parent  ngOnInit
parent  ngDoCheck
parent  ngAfterContentInit
parent  ngAfterContentChecked
child   ngOnChanges
child   ngOnInit
child   ngDoCheck
child   ngAfterContentInit
child   ngAfterContentChecked
child   ngAfterViewInit
child   ngAfterViewChecked
parent  ngAfterViewInit
parent  ngAfterViewChecked

Two things matter. Children initialise before the parent’s view hooks, which is why a parent can safely query its children in ngAfterViewInit. And destruction runs parent first, then children.

Implement the interface

TypeScript does not require it, but implement the interface anyway.

export class UserList implements OnInit, OnDestroy {
  ngOnInit(): void { ... }
  ngOnDestroy(): void { ... }
}

The compiler then catches a misspelt ngOnint, which otherwise fails silently at runtime.

The modern equivalents

ngOnChanges           ->  computed() or effect() on a signal input
ngOnInit              ->  a field initialiser, or httpResource
ngAfterViewInit       ->  afterNextRender
ngAfterContentInit    ->  contentChildren, which is reactive
ngAfterViewChecked    ->  afterRenderEffect
ngOnDestroy           ->  DestroyRef.onDestroy, or effect cleanup
ngDoCheck             ->  nothing, you should not need it

A modern component often has no lifecycle hooks at all.

Quick recap

  • The hooks still work but signals made most of them unnecessary.
  • ngOnChanges is replaced by computed and effect on signal inputs.
  • ngAfterViewInit is replaced by afterNextRender, which skips the server.
  • ngOnDestroy is replaced by DestroyRef.onDestroy, effect cleanup or takeUntilDestroyed.
  • Never mix ngOnChanges with signal inputs, convert a component’s inputs together.
  • Avoid ngDoCheck and the Checked hooks, they run constantly.
  • Implement the interfaces so a misspelt hook is a compile error.
app.ts

import { Component, OnInit, OnDestroy, inject, input, signal, computed,
         effect, viewChild, contentChildren, ElementRef, DestroyRef,
         afterNextRender } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
// A MODERN component often has no lifecycle hooks at all.
@Component({ selector: 'app-chart', template: `<canvas #canvas></canvas>` })
export class Chart {
  readonly data = input.required<number[]>();
  private readonly canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
  // replaces ngOnChanges: depends only on what it reads, no string keys
  constructor() {
    effect(() => this.redraw(this.data()));
    // replaces ngAfterViewInit, and does NOT run on the server
    afterNextRender(onCleanup => {
      const chart = new ThirdPartyChart(this.canvas().nativeElement);
      onCleanup(() => chart.destroy());        // cleanup beside the setup
    });
    // replaces ngOnDestroy for subscriptions
    this.updates$.pipe(takeUntilDestroyed()).subscribe(u => this.handle(u));
    // replaces ngOnDestroy for timers
    const id = setInterval(() => this.poll(), 1000);
    inject(DestroyRef).onDestroy(() => clearInterval(id));
  }
  private redraw(data: number[]) {}
  private handle(u: unknown) {}
  private poll() {}
  private readonly updates$ = someObservable();
}
// The old way, still valid and still everywhere. Implement the interface
// so a misspelt ngOnint is a COMPILE error rather than a silent no-op.
@Component({ selector: 'app-old', template: `` })
export class Old implements OnInit, OnDestroy {
  private id?: ReturnType<typeof setInterval>;
  ngOnInit(): void { this.id = setInterval(() => {}, 1000); }
  ngOnDestroy(): void { clearInterval(this.id); }
}
// Modern equivalents:
//   ngOnChanges        -> computed() / effect() on a signal input
//   ngOnInit           -> a field initialiser, or httpResource
//   ngAfterViewInit    -> afterNextRender
//   ngAfterContentInit -> contentChildren (already reactive)
//   ngAfterViewChecked -> afterRenderEffect
//   ngOnDestroy        -> DestroyRef.onDestroy / effect cleanup / takeUntilDestroyed
//   ngDoCheck          -> nothing. It runs on EVERY pass. Avoid it.
// NEVER mix ngOnChanges with signal inputs. It does not fire reliably.

Was this explanation helpful?

Your feedback helps us improve the content for everyone.