DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Angular 20: Mastering the Component Lifecycle (Zoneless Ready, 2025 Edition)

Angular 20: Mastering the Component Lifecycle (Zoneless Ready, 2025 Edition)

TL;DR — Angular 20’s zoneless runtime means you can think about lifecycle hooks as pure, predictable events. Hooks like ngOnInit, ngOnDestroy, and the new afterNextRender fit naturally into the signal‑based, reactive model of modern Angular. Let’s explore every stage — from creation to destruction — using real code you can paste and watch come to life.


Introduction: Lifecycle Reimagined in Angular 20

Angular’s component lifecycle defines the sequence of steps from creation to destruction. Each hook represents a checkpoint in rendering and change detection.

In Angular 20, lifecycle hooks are more predictable, signal-friendly, and zone‑less compatible. Combined with new rendering callbacks (afterNextRender, afterEveryRender) you gain granular control without NgZone.


The Big Picture — Lifecycle Flow

Phase Hook Summary
Creation constructor() Initializes the component class. Avoid side effects.
Change Detection ngOnInit() Runs once after inputs initialize.
ngOnChanges() Runs every time inputs change.
ngDoCheck() Custom change detection logic (use sparingly).
Content Projection ngAfterContentInit() Once after projected content initializes.
ngAfterContentChecked() After each check of projected content.
View Initialization ngAfterViewInit() Once after the view initializes.
ngAfterViewChecked() After each check of the view.
Rendering afterNextRender() Once after all components render.
afterEveryRender() Runs every render cycle.
Destruction ngOnDestroy() Cleanup before component removal.

Hands‑on Example — Lifecycle Hooks in Action

Here’s a full working component demonstrating every lifecycle phase, including Signals and Zoneless rendering callbacks:

import {
  afterNextRender,
  Component,
  effect,
  OnChanges,
  OnInit,
  signal,
} from '@angular/core';
import { TitleComponent } from '../../components/title/title.component';

const log = (...messages: string[]) => {
  console.log(`%c${messages.join(' ')}`, 'color: #4ef2af; font-weight: bold');
};

@Component({
  selector: 'app-home-page',
  imports: [TitleComponent],
  templateUrl: './home-page.component.html',
})
export class HomePageComponent implements OnInit, OnChanges {
  traditionalProperty = 'Cristian';
  signalProperty = signal('Cristian');

  constructor() {
    log('constructor()', 'Called first — minimal setup only');
  }

  changeTraditional() {
    this.traditionalProperty = 'Cristian Sifuentes';
  }
  changeSignal() {
    this.signalProperty.set('Cristian Sifuentes');
  }

  basicEffect = effect((onCleanup) => {
    log('effect()', 'Signal side effect triggered');
    onCleanup(() => log('cleanup()', 'Runs when effect is destroyed'));
  });

  ngOnInit() {
    log('ngOnInit()', 'After inputs initialized — setup subscriptions or fetch data');
  }

  ngOnChanges() {
    log('ngOnChanges()', 'Runs when @Input() properties change');
  }

  ngDoCheck() {
    log('ngDoCheck()', 'Manual change detection run');
  }

  ngAfterContentInit() {
    log('ngAfterContentInit()', 'Projected content initialized');
  }

  ngAfterContentChecked() {
    log('ngAfterContentChecked()', 'Projected content checked');
  }

  ngAfterViewInit() {
    log('ngAfterViewInit()', 'View and children initialized');
  }

  ngAfterViewChecked() {
    log('ngAfterViewChecked()', 'View checked for changes');
  }

  ngOnDestroy() {
    log('ngOnDestroy()', 'Cleanup before destruction — unsubscribe, detach listeners');
  }

  afterNextRenderEffect = afterNextRender(() => {
    log('afterNextRender()', 'All components rendered to DOM');
  });
}
Enter fullscreen mode Exit fullscreen mode

Understanding New Hooks: afterNextRender() & afterEveryRender()

These are application‑level render callbacks, not per‑component hooks. They execute once all DOM updates have completed.

Example with phases:

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

@Component({
  selector: 'app-profile',
  template: `<div>User Profile</div>`
})
export class ProfileComponent {
  private el = inject(ElementRef);

  constructor() {
    afterNextRender({
      write: () => {
        const el = this.el.nativeElement;
        el.style.opacity = '1';
        return el.getBoundingClientRect().height;
      },
      read: (height) => {
        console.log('Final element height:', height);
      },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Phases

Phase Description
earlyRead Read layout-affecting properties before any writes.
mixedReadWrite Default phase; both reads and writes allowed.
write Apply layout changes (DOM writes).
read Read layout data after writes (e.g., bounding boxes).

Use phases to avoid layout thrashing and optimize performance in zoneless mode.


Zoneless Best Practices (Angular 20+)

  • Prefer Signals and Effects to trigger updates instead of relying on Zones.
  • Use afterNextRender to coordinate DOM updates safely.
  • Avoid calling detectChanges() manually — rely on the reactive model.
  • Cleanup logic goes in ngOnDestroy or via DestroyRef.
  • Remember: in zoneless Angular, your updates are explicit — this improves performance and predictability.

Cleanup with DestroyRef

import { Component, DestroyRef, inject } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  template: `User profile works!`,
})
export class UserProfile {
  private destroyRef = inject(DestroyRef);

  constructor() {
    this.destroyRef.onDestroy(() => console.log('Destroyed!'));
  }
}
Enter fullscreen mode Exit fullscreen mode

Use DestroyRef to keep setup and teardown close together — ideal for reactive, self-contained services.


Execution Order Overview

During Initialization

constructor()
ngOnChanges()
ngOnInit()
ngDoCheck()
ngAfterContentInit()
ngAfterViewInit()
ngAfterContentChecked()
ngAfterViewChecked()
afterNextRender()
afterEveryRender()
Enter fullscreen mode Exit fullscreen mode

During Updates

ngOnChanges()
ngDoCheck()
ngAfterContentChecked()
ngAfterViewChecked()
afterEveryRender()
Enter fullscreen mode Exit fullscreen mode

Expert Tips & Pitfalls

Use ngOnInit for async initialization — ideal for signals, HTTP fetches, and event listeners.

Avoid state changes in ngAfter*Checked — they will trigger ExpressionChanged errors.

Leverage afterNextRender for animations or measurement post-DOM update.

DestroyRef > ngOnDestroy when collaborating with external logic.

Prefer signals and computed properties for reactive, lightweight patterns.


Resources


🧠 Angular 20 is about precision — clean lifecycles, explicit renders, and reactive purity. Master these hooks, and you’ll master change detection itself.*

Top comments (1)

Collapse
 
shemith_mohanan_6361bb8a2 profile image
shemith mohanan

Such a clean breakdown of Angular 20’s zoneless lifecycle — especially the new render callbacks and DestroyRef pattern. The framework feels sharper and more explicit than ever. Great work! ⚡