All articles

Angular Lifecycle Hooks Explained

Angular lifecycle hooks are methods Angular calls at defined moments as it creates, updates, and destroys a component. In Angular 22 the hooks you reach for…

0 · log in to like, save & follow Share on LinkedIn Share on X

Angular lifecycle hooks are methods Angular calls at defined moments as it creates, updates, and destroys a component. In Angular 22 the hooks you reach for most are ngOnInit for setup, ngOnDestroy for cleanup, and the new render hooks afterNextRender() and afterRender() for DOM work — while signals (signal, computed, effect) quietly remove the need for many hooks you once wrote by hand. This article walks through every hook, the exact order they fire, and when a signal replaces one entirely.

Angular Lifecycle Hooks Explained

The lifecycle in one picture

A component's life runs in three phases: it is constructed, then repeatedly checked (change detection runs and the view updates), and finally destroyed. Angular exposes hooks at each transition. You opt in by implementing the matching interface (OnInit, OnDestroy, and so on) and defining the method. The interfaces are optional at runtime, but implementing them gives you compile-time safety and clear intent.

Since Angular 22 ships standalone components by default (no NgModules) and defaults to a zoneless, signal-first change detection model, the hooks still exist — but you will write far fewer of them. Understanding the hooks still matters: you will read them in existing code, debug ordering issues, and integrate libraries that hook into specific moments. The mental model to carry through this article is simple — hooks mark boundaries (a component appearing, its DOM painting, its removal), while signals handle everything that changes continuously in between.

constructor vs ngOnInit

The constructor runs when JavaScript instantiates the class. At that point Angular has not yet set the component's inputs and has not rendered anything. Use the constructor only for dependency injection via inject() and for wiring that needs no inputs. Trying to read an input in the constructor gives you undefined, which is the single most common lifecycle mistake newcomers hit.

ngOnInit runs once, after Angular has set the initial input values and completed the first change detection pass. This is where initialization that depends on inputs belongs — fetching data keyed by an input, deriving initial state, and so on.

import { Component, OnInit, inject } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-profile',
  standalone: true,
  template: `<h2>{{ name }}</h2>`,
})
export class ProfileComponent implements OnInit {
  private users = inject(UserService);
  name = '';

  ngOnInit(): void {
    // inputs are ready here, not in the constructor
    this.name = this.users.currentName();
  }
}

Rule of thumb: inject in the constructor, initialize in ngOnInit.

ngOnChanges, and why signal inputs plus effect() replace it

ngOnChanges fires before ngOnInit and again whenever a decorator-based @Input() value changes. It receives a SimpleChanges map of previous and current values. It is the classic way to react to changing inputs.

In Angular 22 the idiomatic input is the signal-based input() function, and reacting to a changed input no longer needs ngOnChanges at all. A computed() derives new state automatically, and an effect() runs side effects when the input changes:

import { Component, input, computed, effect } from '@angular/core';

@Component({
  selector: 'app-price',
  standalone: true,
  template: `{{ withTax() }}`,
})
export class PriceComponent {
  amount = input.required<number>();
  withTax = computed(() => this.amount() * 1.2);

  constructor() {
    effect(() => console.log('amount changed:', this.amount()));
  }
}

The computed and the effect re-run only when amount actually changes — no SimpleChanges bookkeeping, no manual comparison. Reach for ngOnChanges only when you still use decorator inputs.

ngOnDestroy, DestroyRef, and takeUntilDestroyed

ngOnDestroy runs once, right before Angular removes the component. Use it to release anything that outlives the view: subscriptions, timers, event listeners, and open connections.

Angular 22 gives you two cleaner options. DestroyRef lets you register a teardown callback from anywhere, including inside the constructor. And takeUntilDestroyed() unsubscribes an RxJS stream automatically when the component is destroyed:

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

@Component({ selector: 'app-clock', standalone: true, template: `{{ ticks }}` })
export class ClockComponent {
  private destroyRef = inject(DestroyRef);
  ticks = 0;

  constructor() {
    interval(1000)
      .pipe(takeUntilDestroyed())
      .subscribe(() => this.ticks++);

    this.destroyRef.onDestroy(() => console.log('clock torn down'));
  }
}

This removes most hand-written ngOnDestroy methods and the leaky bugs they invited. Because takeUntilDestroyed() reads the current destroy context, call it during construction (or pass a DestroyRef explicitly if you call it later). DestroyRef.onDestroy() is the general-purpose escape hatch: register any teardown function and Angular runs it exactly once when the component is torn down, no interface required.

View and content hooks

Four hooks report on the rendered tree:

  • ngAfterContentInit — projected content (via <ng-content>) has been initialized. Fires once.
  • ngAfterContentChecked — projected content has been checked. Fires after every check.
  • ngAfterViewInit — the component's own view and its child views are initialized. Fires once; this is where viewChild() results are safely available.
  • ngAfterViewChecked — the view and children have been checked. Fires after every check.

The Checked variants run on every change detection cycle, so keep their bodies cheap — heavy work there is a common performance trap.

The render hooks: afterNextRender and afterRender

Traditional view hooks can run during server-side rendering, where the DOM does not exist. Angular 22's render hooks solve this: they run only in the browser, after the DOM is painted.

  • afterNextRender(callback) — runs once, after the next render. Ideal for one-time DOM measurement or integrating a third-party library that needs a real element.
  • afterRender(callback) — runs after every render. Use sparingly.
  • afterRenderEffect() — a signal-aware variant that re-runs when its tracked signals change, letting you sync imperative DOM to reactive state.
import { Component, ElementRef, afterNextRender, inject } from '@angular/core';

@Component({ selector: 'app-chart', standalone: true, template: `<canvas></canvas>` })
export class ChartComponent {
  private host = inject(ElementRef);

  constructor() {
    afterNextRender(() => {
      const canvas = this.host.nativeElement.querySelector('canvas');
      // safe: real DOM exists, browser only
      canvas.getContext('2d');
    });
  }
}

Place DOM-measuring or canvas code here instead of ngAfterViewInit so it stays SSR-safe.

What order do the hooks fire?

For the first render, the sequence is:

  1. constructor
  2. ngOnChanges (if it has inputs)
  3. ngOnInit
  4. ngAfterContentInit
  5. ngAfterContentChecked
  6. ngAfterViewInit
  7. ngAfterViewChecked
  8. afterNextRender / afterRender (browser only)

On later change detection cycles, ngOnChanges (when inputs changed) runs first, then the Checked hooks, then the render hooks. At teardown, ngOnDestroy runs last. A parent completes its own content and view hooks only after its children have finished theirs, so in a tree the deepest child's ngAfterViewInit fires before its parent's. The companion example below logs each hook, so you can watch this exact order print in the browser console and confirm the sequence for yourself rather than memorizing it.

TypeScript standalone component logging each Angular 22 lifecycle hook in order

Why signals reduce the need for lifecycle wiring

Before signals, hooks were the only way to react to change: ngOnChanges for inputs, ngAfterViewChecked for recomputation, manual subscriptions torn down in ngOnDestroy. Angular 22's reactive primitives collapse much of that:

  • computed() replaces derived-state recalculation you used to trigger in ngOnChanges or a checked hook.
  • effect() replaces side-effect reactions to input or state changes, and cleans itself up automatically.
  • takeUntilDestroyed() and DestroyRef replace most ngOnDestroy bodies.

The result: hooks are now for genuine lifecycle boundaries (init, destroy, first DOM paint), while ongoing reactivity lives in signals. A practical migration heuristic — if you find yourself comparing old and new values inside ngOnChanges, or manually re-running a calculation inside a checked hook, that logic almost always belongs in a computed() or effect() instead. The payoff is not just fewer lines but fewer ordering bugs, since signals recompute in dependency order rather than in the fixed hook sequence you have to reason about by hand.

Key takeaways

  • Inject dependencies in the constructor; do input-dependent setup in ngOnInit.
  • Prefer signal input() with computed()/effect() over ngOnChanges.
  • Use takeUntilDestroyed() and DestroyRef instead of hand-written ngOnDestroy for subscriptions.
  • Keep the Checked hooks cheap — they run on every change detection cycle.
  • Do browser DOM work in afterNextRender() / afterRender(), not view hooks, for SSR safety.
  • In Angular 22, signals handle ongoing reactivity so hooks mark true lifecycle boundaries.

Frequently asked questions

Are lifecycle hooks deprecated in Angular 22?

No. The interfaces and hooks remain fully supported. Signals reduce how often you need ngOnChanges and ngOnDestroy, but ngOnInit, the view hooks, and the render hooks are still standard tools.

What is the difference between ngAfterViewInit and afterNextRender?

ngAfterViewInit fires once after the component's view initializes and can run during server-side rendering. afterNextRender() runs only in the browser after the DOM is painted, making it the safe place for direct DOM manipulation.

When should I still use ngOnChanges?

Use it when you keep decorator-based @Input() properties and need the previous-versus-current SimpleChanges values. With the signal input() function, prefer computed() and effect() instead.

Do I need ngOnDestroy if I use takeUntilDestroyed?

Usually not for RxJS subscriptions — takeUntilDestroyed() unsubscribes automatically. Keep ngOnDestroy (or DestroyRef.onDestroy) for non-RxJS cleanup such as clearing timers or detaching event listeners.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.