Angular Component Lifecycle Hooks in 2026: Every Hook, effect(), and afterNextRender [2026]

Link copied
Angular Component Lifecycle Hooks in 2026: Every Hook, effect(), and afterNextRender [2026]

Angular Component Lifecycle Hooks in 2026: Every Hook, effect(), and afterNextRender [2026]

Angular components go through a predictable sequence of moments — created, inputs received, view rendered, content projected, destroyed. The framework gives you nine hooks to react to those moments, but in 2026 a signal-first codebase reaches for only two or three of them. This lesson explains every hook, the exact order they fire, and which ones the modern reactive primitives (effect(), afterNextRender()) have effectively retired.

This is lesson 2.7 of the Angular Tutorial. It assumes you have completed lesson 2.1 (component anatomy) and lesson 2.4 (input signals) — both are referenced below.

The full hook list, in firing order #

Every Angular component goes through these phases. The hook (if you implement it) runs at each step:

Order Hook When it fires Modern alternative
1 constructor Class instantiated, DI injected Still essential — use inject() here
2 ngOnChanges Inputs change (also on first set) Signals fire reactively; rarely needed
3 ngOnInit Once, after first ngOnChanges constructor works in most cases
4 ngDoCheck Every CD cycle, before content check Avoid — use signals
5 ngAfterContentInit Once, after content projection ready contentChild() signal queries
6 ngAfterContentChecked Every CD cycle, after content check Avoid
7 ngAfterViewInit Once, after view (template) rendered afterNextRender() or viewChild()
8 ngAfterViewChecked Every CD cycle, after view check afterRender() if needed
9 ngOnDestroy Component about to be destroyed DestroyRef.onDestroy() or effect() cleanup

Learning the order matters once. Day to day, you only need the constructor, effect(), afterNextRender(), and DestroyRef.

What modern Angular actually uses #

The four modern primitives that replace the nine legacy hooks:

inject() in the constructor #

Dependency injection runs in the constructor. Field initializers (x = inject(Y)) effectively run there too.

export class CartComponent {
  private auth = inject(AuthService);
  private http = inject(HttpClient);
  // No need for ngOnInit — initialization happens at construction
}

effect() for side effects #

effect() from @angular/core runs whenever any signal it reads changes. It replaces ngOnChanges for the vast majority of cases:

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

export class UserStatsComponent {
  user = input.required<User>();

  constructor() {
    // Replaces ngOnChanges + manual change tracking
    effect(() => {
      console.log(`User changed to ${this.user().name}`);
      analytics.track('user_viewed', { id: this.user().id });
    });
  }
}

Key behaviors of effect():

  • Runs once on creation, then again whenever a tracked signal changes
  • Tracks dependencies automatically — any signal read inside the function becomes a dependency
  • Cleanup function: return a function from inside effect(), it runs before the next execution and on destroy
  • Must be called in an injection context (constructor or field initializer)
effect((onCleanup) => {
  const handle = setInterval(() => console.log(this.user().name), 1000);
  onCleanup(() => clearInterval(handle));
});

afterNextRender() for DOM measurements #

When you need to read computed DOM properties (heights, scroll positions, element bounding rectangles), the timing matters. The view must be in the DOM and laid out. afterNextRender() is the modern primitive for this:

import { afterNextRender, ElementRef, inject } from '@angular/core';

export class StickyHeaderComponent {
  private host = inject(ElementRef<HTMLElement>);

  constructor() {
    afterNextRender(() => {
      const height = this.host.nativeElement.offsetHeight;
      document.documentElement.style.setProperty('--header-h', `${height}px`);
    });
  }
}

Replaces ngAfterViewInit for any case where you need access to the rendered DOM.

DestroyRef.onDestroy() for cleanup #

DestroyRef is the modern, injectable cleanup hook:

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

export class ResizeWatcherComponent {
  private destroyRef = inject(DestroyRef);

  constructor() {
    fromEvent(window, 'resize')
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => console.log('resized'));

    // Or use the raw cleanup callback
    this.destroyRef.onDestroy(() => console.log('component dying'));
  }
}

No more OnDestroy interface, no more manual unsubscribe(). The takeUntilDestroyed() operator handles RxJS cleanup; onDestroy() handles everything else.

Comparing legacy vs modern #

Want Legacy way Modern way
Run code once at startup ngOnInit constructor (or effect() if it reads signals)
React to input changes ngOnChanges effect() reading input signals
Read DOM after render ngAfterViewInit afterNextRender()
Cleanup on destroy ngOnDestroy + manual unsubscribe DestroyRef.onDestroy() + takeUntilDestroyed()
Reference child component @ViewChild() + ngAfterViewInit viewChild() signal (lesson 2.13)
Reference projected content @ContentChild() + ngAfterContentInit contentChild() signal (lesson 2.13)

The modern pattern is shorter, type-safer, and reactive. Use it for new code.

When you DO still need a legacy hook #

A handful of cases still warrant the old hooks:

  1. ngOnChanges with SimpleChanges — when you need the previous value of an input, not just the current one. Signals only expose the current value. (You can also store previous in a computed chain, but it's less ergonomic.)
  2. ngDoCheck — extremely rare. The custom CD path for non-signal libraries that mutate their data in place.
  3. ngAfterViewChecked — rare. Reading layout after every CD cycle (most cases want afterRender() instead).

If you find yourself reaching for these, double-check whether a signal-based approach would work. Nine times out of ten, it does.

The full firing order, visualized #

For a child component being created and inserted under a parent:

1. Parent's CD tick begins
2. New child component created
3. Child constructor runs (DI, field initializers)
4. Inputs set (any `input()` signals now have parent's values)
5. Child's ngOnChanges runs (legacy) with the initial values
6. Child's ngOnInit runs (legacy)
7. Child's ngDoCheck runs (legacy)
8. Child's content children projected
9. Child's ngAfterContentInit + ngAfterContentChecked run (legacy)
10. Child's view template renders
11. Child's ngAfterViewInit + ngAfterViewChecked run (legacy)
12. Child's afterNextRender callbacks run (modern)
13. Child's effect() callbacks fire for any signals read during the above

On subsequent CD cycles, steps 5, 7, 9, 11 may re-fire; steps 12 (afterNextRender) only on render; effects fire whenever their tracked signals change.

On destroy:

1. Component removed from DOM
2. effect() cleanup functions run (in reverse-registration order)
3. DestroyRef.onDestroy() callbacks run (in reverse-registration order)
4. ngOnDestroy runs (legacy)
5. Component reference released for GC

A realistic component using only modern primitives #

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

@Component({
  selector: 'app-clock',
  template: `<time>{{ now() }}</time>`,
})
export class ClockComponent {
  format = input<'short' | 'long'>('short');
  private host = inject(ElementRef<HTMLTimeElement>);
  private destroyRef = inject(DestroyRef);

  protected now = signal(new Date().toLocaleString());

  constructor() {
    // Update every second; auto-cleanup on destroy
    interval(1000)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => this.now.set(this.formatNow()));

    // React to format input changes
    effect(() => {
      console.log(`Format changed to ${this.format()}`);
      this.now.set(this.formatNow());
    });

    // Run once after first render — measure DOM
    afterNextRender(() => {
      console.log(`Clock width: ${this.host.nativeElement.offsetWidth}px`);
    });
  }

  private formatNow() {
    return this.format() === 'short'
      ? new Date().toLocaleTimeString()
      : new Date().toLocaleString();
  }
}

Not an ngOnInit, ngOnDestroy, or ngOnChanges in sight. All initialization happens in the constructor; all reactivity goes through effect(); all cleanup is automatic.

Common gotchas #

Symptom Cause Fix
effect() must be called within an injection context Called effect() outside the constructor/field initializer Move it into the constructor, or capture an Injector and wrap with runInInjectionContext
afterNextRender runs but DOM not yet ready You forgot it runs once, not on every render Use afterRender() if you need it every cycle (rare)
Memory leak from leftover subscription Forgot takeUntilDestroyed() Always pipe RxJS subscriptions through takeUntilDestroyed(destroyRef)
ngOnChanges not firing on input() signals input() doesn't trigger ngOnChanges — it's a signal Use effect() instead, or migrate the input back to @Input() (not recommended)
Constructor runs in tests but DOM isn't ready Tests bypass the render cycle TestBed.tick() to advance to first render, then read DOM

What's next #

Lesson 2.8 covers pipes — built-in pipes plus custom pipes you write yourself. Lessons 2.9–2.14 cover directives, ng-template, styling, host metadata, signal queries, and programmatic component creation — completing the Components & Templates module.

After Module 2, Module 3 dives into the signal system in depth. The effect() you saw in this lesson gets its own dedicated lesson there, with timing rules, glitch-free guarantees, and untracked().

Try it yourself #

Replace ngOnInit + ngOnDestroy + a manual subscription with the modern primitives:

// Legacy — pre-v17 patterns
export class WebSocketCardComponent implements OnInit, OnDestroy {
  private ws?: WebSocket;
  ngOnInit() { this.ws = new WebSocket('wss://example.com'); }
  ngOnDestroy() { this.ws?.close(); }
}

// Modern — 2026 patterns
export class WebSocketCardComponent {
  constructor() {
    const ws = new WebSocket('wss://example.com');
    inject(DestroyRef).onDestroy(() => ws.close());
  }
}

The modern version has no interfaces to implement, no separate methods, no field that's ? because it might not be set yet. The whole lifecycle is one constructor.

YouI need to call my init logic AFTER the inputs are set, not in the constructor. Is ngOnInit still the right answer?
Claude · used get_best_practicesFor input() signals specifically — no, the constructor works. Inputs are signals, so reading this.myInput() in effect() (set up in the constructor) gives you the current value AND re-runs on every change. The constructor body runs before inputs are set, but anything inside effect() or computed() reads them lazily — at the right time. ngOnInit is only necessary if you’re still using @Input() decorators.

Lesson 2.8 picks up pipes — the cleanest way to transform values for display.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied When Angular is launched ? When Angular Landed: A History of the Popular Web Framework # Angular, …

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *