| |

Angular 16 🅰️ Advanced Lifecycle — Changes, Content, and View Hooks

Chapter 15 covered the three hooks you use every day: constructor, ngOnInit, and ngOnDestroy. This chapter goes deeper. Angular has eight lifecycle hooks, and the remaining five — ngOnChanges, ngDoCheck, ngAfterContentInit, ngAfterContentChecked, ngAfterViewInit, ngAfterViewChecked — handle the phases most components never need but some depend on entirely. They’re the hooks that fire when inputs change, when the view appears, when projected content initializes, and on every change detection cycle. Knowing when each fires and what’s available at that moment is what separates components that work by accident from components that work by design.

Key point: The full lifecycle has a specific order. ngOnChanges fires before ngOnInit and again on every input change. ngAfterContentInit fires once after projected content initializes. ngAfterViewInit fires once after the component’s own view initializes. ngDoCheck, ngAfterContentChecked, and ngAfterViewChecked fire on every change detection cycle. The “checked” hooks run constantly — keep them cheap. The “init” hooks run once — that’s where setup belongs.


The full lifecycle order

Here’s the complete order for a component’s first render:

1.  constructor
2.  ngOnChanges (first)
3.  ngOnInit
4.  ngDoCheck
5.  ngAfterContentInit
6.  ngAfterContentChecked
7.  ngAfterViewInit
8.  ngAfterViewChecked

On every subsequent change detection cycle:

ngOnChanges (if inputs changed)
ngDoCheck
ngAfterContentChecked
ngAfterViewChecked

On destruction:

ngOnDestroy

When each runs:

HookRuns
constructorOnce, before inputs
ngOnChangesFirst time and on every input change
ngOnInitOnce, after first ngOnChanges
ngDoCheckEvery CD cycle
ngAfterContentInitOnce, after content initializes
ngAfterContentCheckedEvery CD cycle after content checked
ngAfterViewInitOnce, after view initializes
ngAfterViewCheckedEvery CD cycle after view checked
ngOnDestroyOnce, before destruction

The pattern: “Init” hooks run once; “checked” hooks run on every cycle; ngOnChanges runs when inputs change; ngOnDestroy runs when the component goes away.

Why order matters: Each hook has access to a specific state. ngOnChanges has the changes object. ngOnInit has inputs but not the view. ngAfterViewInit has the view and its queries resolved. Trying to use something before its hook fails — that’s the practical reason to know the order.

Why so many hooks: Each phase of a component’s life has a specific state that some component somewhere needs. ngAfterViewInit is where @ViewChild queries are resolved. ngOnChanges is where input changes are available. The framework exposes the phases; you pick the one that matches what you need.


ngOnChanges — reacting to input changes

ngOnChanges fires before ngOnInit and again whenever an @Input property changes.

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `<h2>{{ name }}</h2>`
})
export class UserCardComponent implements OnChanges {
  @Input() userId!: number;
  @Input() name = '';

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['userId']) {
      const { previousValue, currentValue, firstChange } = changes['userId'];
      console.log(`userId: ${previousValue} → ${currentValue}`);
      if (firstChange) {
        console.log('First assignment');
      }
    }
  }
}

The SimpleChanges object:

{
  userId: {
    previousValue: undefined,   // undefined on first change
    currentValue: 42,
    firstChange: true
  },
  name: {
    previousValue: '',
    currentValue: 'Alice',
    firstChange: true
  }
}

Each key is an input name. The value has the previous value, the current value, and a firstChange flag.

When it fires:

  • Once before ngOnInit, for the initial values
  • Again on every subsequent change to any input

When it does not fire:

  • If no inputs change
  • If a signal input changes (signal inputs use effect or computed instead)

Checking whether a specific input changed:

ngOnChanges(changes: SimpleChanges): void {
  if (changes['theme']) {
    this.applyTheme(changes['theme'].currentValue);
  }
}

Always check if (changes['x']) before accessing — the object only contains inputs that changed.

First change vs subsequent:

ngOnChanges(changes: SimpleChanges): void {
  const change = changes['userId'];
  if (change?.firstChange) {
    this.loadInitialData(change.currentValue);
  } else {
    this.refresh(change.currentValue);
  }
}

firstChange distinguishes setup from updates.

Inputs that don’t fire ngOnChanges:

  • Signal inputs (input()) — these don’t participate in ngOnChanges; use effect or computed instead
  • Values set directly on the component instance (not via binding)
  • Same reference — if an object is mutated in place, ngOnChanges doesn’t fire

The last caveat is important: Angular compares by reference. user.name = 'Bob' doesn’t fire ngOnChanges; user = { ...user, name: 'Bob' } does.

Why ngOnChanges matters: It’s the only hook that gives you the previous value of an input. If you need to compare before and after, or handle first-change differently from updates, it’s the only place. ngOnInit runs once; ngOnChanges runs on every change.

Why signal inputs replaced this: Signal inputs (input()) are reactive by nature — reading them in a computed or effect re-runs when they change. That’s more ergonomic than ngOnChanges with its SimpleChanges object. New code should prefer signal inputs for reactive behavior; ngOnChanges remains for decorator-based inputs.


ngDoCheck — custom change detection

ngDoCheck fires on every change detection cycle — after ngOnChanges and ngOnInit, before ngAfterContentInit, and on every subsequent cycle.

import { Component, DoCheck, Input } from '@angular/core';

@Component({
  selector: 'app-tracker',
  standalone: true,
  template: `<p>{{ items.length }} items</p>`
})
export class TrackerComponent implements DoCheck {
  @Input() items: string[] = [];
  private previousCount = 0;

  ngDoCheck(): void {
    if (this.items.length !== this.previousCount) {
      console.log(`Count changed: ${this.previousCount} → ${this.items.length}`);
      this.previousCount = this.items.length;
    }
  }
}

Angular doesn’t detect changes inside arrays or objects by reference. ngDoCheck lets you compare manually — a custom change detection.

When to use ngDoCheck:

  • When Angular’s reference-based change detection misses a change (mutation inside an object or array)
  • When you need a custom equality check
  • When you’re integrating with a library that mutates data externally

When NOT to use it:

  • Almost always. ngDoCheck runs on every CD cycle — potentially dozens of times per second. Expensive work here destroys performance.
  • Prefer computed signals for derived state
  • Prefer immutable updates so reference detection works

The risk: ngDoCheck runs before the view is checked. Modifying state here can cause ExpressionChangedAfterItHasBeenChecked errors — Angular sees the value change between when it was set and when it was rendered.

Example of the risk:

ngDoCheck(): void {
  this.count = this.items.length;  // ❌ can cause ExpressionChanged errors
}

The fix is usually to restructure — compute the value once, update it in response to events, or use a signal.

Pairing with KeyValueDiffers and IterableDiffers: Angular provides services to detect changes within objects and arrays. ngDoCheck is where you call them.

constructor(private differs: IterableDiffers) {
  this.differ = this.differs.find([]).create();
}

ngDoCheck(): void {
  const changes = this.differ.diff(this.items);
  if (changes) {
    changes.forEachAddedItem(r => console.log('Added', r.item));
    changes.forEachRemovedItem(r => console.log('Removed', r.item));
  }
}

That’s the sophisticated use case for ngDoCheck — diffing collections.

Why ngDoCheck exists: Angular’s default change detection is reference-based. Most code should keep data immutable so references change. But when you can’t — because a library mutates in place, or you’re processing a growing collection — ngDoCheck is the escape hatch. Use it sparingly.

Why ngDoCheck is a last resort: Every other hook has a specific trigger. ngDoCheck runs always. That makes it the least efficient place to put logic. If you find yourself reaching for it, ask: can I make this data immutable, or use a signal instead? If yes, prefer that. ngDoCheck is for the cases that can’t be restructured.


ngAfterContentInit — projected content ready

ngAfterContentInit fires once, after Angular projects external content into the component.

import { Component, ContentChild, AfterContentInit } from '@angular/core';

@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <ng-content></ng-content>
    </div>
  `
})
export class CardComponent implements AfterContentInit {
  @ContentChild('title') titleRef?: ElementRef;

  ngAfterContentInit(): void {
    console.log('Projected content ready:', this.titleRef?.nativeElement);
  }
}

The parent’s content is projected into <ng-content>. By the time ngAfterContentInit runs, @ContentChild queries are resolved.

When it fires:

  • Once, after ngDoCheck on the first cycle
  • After projected content is initialized
  • Before the component’s own view is initialized

What’s available:

  • @ContentChild and @ContentChildren queries are resolved
  • The projected content’s directives and components exist

What’s not available:

  • The component’s own view — @ViewChild queries aren’t resolved yet
  • The DOM is not fully rendered

Only fires when there’s projected content: If the component has no <ng-content>, or nothing is projected, the hook still fires but the queries are empty.

Common use: Reading initial values from projected content.

@ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;

ngAfterContentInit(): void {
  this.tabs.forEach(tab => tab.index = this.tabs.toArray().indexOf(tab));
}

The TabsComponent reads its projected TabComponents here.

@ContentChild vs @ViewChild: Content children come from the parent (projected via <ng-content>). View children come from the component’s own template. Different sources, different hooks.

Why ngAfterContentInit matters: It’s when projected content is ready. If your component reads or manipulates projected children, this is the hook. Before it, the queries are empty; after it, they’re set.

Why the content/view distinction: Angular separates “what my parent gave me” (content) from “what my own template declares” (view). Content is projected; view is owned. ngAfterContentInit handles the first, ngAfterViewInit the second. Keeping them separate lets Angular resolve each in the right order.


ngAfterViewInit — view ready

ngAfterViewInit fires once, after the component’s own view is initialized.

import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <input #input type="text">
    <button (click)="focus()">Focus</button>
  `
})
export class SearchComponent implements AfterViewInit {
  @ViewChild('input') inputRef!: ElementRef<HTMLInputElement>;

  ngAfterViewInit(): void {
    this.inputRef.nativeElement.focus();
  }
}

Before ngAfterViewInit, @ViewChild queries are undefined. Inside it, they’re resolved. That’s why DOM manipulation belongs here.

When it fires:

  • Once, after ngAfterContentInit and ngAfterContentChecked
  • After the component’s view — including children — is initialized
  • Before ngAfterViewChecked

What’s available:

  • @ViewChild and @ViewChildren queries resolved
  • The DOM is rendered
  • Child components exist

What to do here:

  • Focus an input
  • Measure an element’s size
  • Initialize a chart or canvas
  • Call a method on a child component
  • Set up a third-party library that needs the DOM

Common pitfalls:

  • Changing state that affects the view — can cause ExpressionChangedAfterItHasBeenCheckedError. Use setTimeout or ChangeDetectorRef.detectChanges() if you must.
  • Assuming @ViewChild is defined before this hook — it isn’t.
  • Assuming it re-runs — it doesn’t. It fires once per component instance.

Example with a chart:

@ViewChild('chart') chartRef!: ElementRef<HTMLCanvasElement>;

ngAfterViewInit(): void {
  const canvas = this.chartRef.nativeElement;
  this.chart = new Chart(canvas, {
    type: 'line',
    data: this.chartData
  });
}

The chart is initialized once the canvas element exists.

Async timing: If @ViewChild uses a template that includes @if, the query may be undefined at ngAfterViewInit if the condition is false. Use @ViewChild with { static: false } (the default) for queries inside conditions, and static: true for those known at compile time.

Why ngAfterViewInit matters: It’s the moment the component’s view — and its children — exist. Everything that needs the rendered DOM belongs here: focus, measurement, third-party libraries. Trying to do these in ngOnInit fails because the view doesn’t exist yet.

Why the view isn’t ready in ngOnInit: ngOnInit runs before Angular has rendered the component’s template. The view — its child components, its DOM nodes — doesn’t exist. ngAfterViewInit runs after rendering. That’s why DOM-related work belongs there and not earlier.


The “checked” hooks

Three hooks fire on every change detection cycle: ngDoCheck, ngAfterContentChecked, ngAfterViewChecked. They’re the most expensive and the most easily misused.

ngAfterContentChecked: Fires after Angular checks the projected content.

ngAfterContentChecked(): void {
  // Runs on every CD cycle
}

ngAfterViewChecked: Fires after Angular checks the component’s view.

ngAfterViewChecked(): void {
  // Runs on every CD cycle
}

When to use them:

  • Almost never. If you need something done on every cycle, look for a reactive alternative — computed, effect, or a signal.
  • The classic use is ngAfterViewChecked with a manual ChangeDetectorRef.detectChanges() for updates that must happen during view check — but this is almost always a workaround for something else.

Why they’re dangerous: Every keystroke, mouse move, and timer tick triggers a CD cycle. If ngAfterViewChecked does real work, the app slows down. Worse, if it triggers another CD cycle (e.g., by changing state), you can create an infinite loop.

The classic infinite loop:

ngAfterViewChecked(): void {
  this.value = Math.random();  // ❌ changes state → new CD cycle → infinite
}

Angular detects the loop and throws after a threshold, but the app is broken.

When they’re legitimately needed: Third-party integrations that need to be resynced on every change. Even then, prefer an effect in modern Angular.

Signal-based alternative:

// Instead of ngAfterViewChecked
someValue = computed(() => /* derived */);
effect(() => {
  // runs when tracked signals change
});

Effects run when their dependencies change, not on every CD cycle. That’s the modern way to handle reactive updates.

Why “checked” hooks exist: They were the only mechanism for post-render synchronization before signals and effects. Modern Angular provides better tools. Use “checked” hooks only when nothing else fits — which is rare.

Why they’re the last resort: “Checked” hooks run constantly. Any work there multiplies by the CD cycle count. Effects and computed signals give you reactivity without the constant execution. If you’re reaching for a “checked” hook, ask: is there a reactive way? Almost always, yes.


Reading inputs across hooks

Inputs are available at different stages, and knowing which is which prevents errors.

HookInputs available?
constructor❌ Not set
ngOnChanges✅ Via changes object
ngOnInit✅ Direct access
ngDoCheck
ngAfterContentInit
ngAfterViewInit
ngOnDestroy

Signal inputs vs decorator inputs:

Aspect@Input()input()
Available in constructor✅ (but value set later)
ngOnChanges fires
Reactive readsManual✅ In computed/effect
Available in ngOnInit
RecommendedLegacyModern

Reading a signal input in ngOnInit:

userId = input.required<number>();

ngOnInit(): void {
  const id = this.userId();  // ✅ value available
  this.load(id);
}

Reacting to signal input changes:

userId = input.required<number>();

constructor() {
  effect(() => {
    this.load(this.userId());  // runs when userId changes
  });
}

The effect replaces ngOnChanges. It runs when the signal changes — including the first time.

Reading decorator inputs in ngOnChanges:

@Input() userId!: number;

ngOnChanges(changes: SimpleChanges): void {
  if (changes['userId']) {
    this.load(changes['userId'].currentValue);
  }
}

That’s the classic pattern for decorator inputs.

Why this matters: Trying to read an input in the constructor returns undefined. Reading a @ViewChild in ngOnInit returns undefined. Knowing which hook has which state is the difference between working code and debugging sessions.

Why inputs are set after the constructor: Angular creates the component, then sets its inputs. The constructor runs during creation; inputs are set afterward. ngOnInit runs after inputs are set. That’s why inputs are available there and not in the constructor — the same reason ngOnChanges fires before ngOnInit.


A full example

A component using the advanced hooks together.

import {
  Component, Input, ViewChild, ContentChild, ElementRef,
  OnChanges, OnInit, DoCheck, AfterContentInit,
  AfterViewInit, OnDestroy, SimpleChanges
} from '@angular/core';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: `
    <header>
      <ng-content select="[title]"></ng-content>
    </header>
    <main #main>
      <p>{{ total }}</p>
    </main>
  `
})
export class DashboardComponent
  implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterViewInit, OnDestroy {

  @Input() items: number[] = [];
  @Input() label = '';

  @ContentChild('title') titleRef?: ElementRef;
  @ViewChild('main') mainRef!: ElementRef<HTMLElement>;

  total = 0;
  private previousItems: number[] = [];

  constructor() {
    console.log('1. constructor');
  }

  ngOnChanges(changes: SimpleChanges): void {
    console.log('2. ngOnChanges', Object.keys(changes));
    if (changes['label']) {
      console.log('   label:', changes['label'].currentValue);
    }
  }

  ngOnInit(): void {
    console.log('3. ngOnInit');
    this.recalculate();
  }

  ngDoCheck(): void {
    console.log('4. ngDoCheck');
    // Check for mutation that reference detection misses
    if (this.items.length !== this.previousItems.length) {
      this.previousItems = [...this.items];
    }
  }

  ngAfterContentInit(): void {
    console.log('5. ngAfterContentInit');
    console.log('   title:', this.titleRef?.nativeElement?.textContent);
  }

  ngAfterViewInit(): void {
    console.log('6. ngAfterViewInit');
    this.mainRef.nativeElement.scrollTop = 0;
  }

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

  private recalculate(): void {
    this.total = this.items.reduce((s, n) => s + n, 0);
  }
}

Parent usage:

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [DashboardComponent],
  template: `
    <app-dashboard [items]="numbers" label="Overview">
      <h1 title>Reports</h1>
    </app-dashboard>
    <button (click)="add()">Add</button>
  `
})
export class AppComponent {
  numbers = [1, 2, 3];
  add(): void {
    this.numbers = [...this.numbers, 4];  // new reference → ngOnChanges fires
  }
}

What runs:

  1. constructor — DI only, no inputs
  2. ngOnChanges — inputs available; label and items change
  3. ngOnInit — recalculates total
  4. ngDoCheck — checks for mutations
  5. ngAfterContentInit — projected title available
  6. ngAfterViewInit — main element exists; scroll set
  7. Later, on Add — ngOnChanges fires again, then ngDoCheck, ngAfterContentChecked, ngAfterViewChecked
  8. On destroy — ngOnDestroy

Each hook does one job. The output order matches the documented lifecycle.

Why this shape: It’s a small dashboard that exercises every hook. ngOnChanges reads inputs, ngOnInit computes, ngDoCheck tracks mutations, ngAfterContentInit reads projected content, ngAfterViewInit touches the DOM, ngOnDestroy cleans up. Seeing them in sequence makes the order concrete.


Complete Example Session

# ============================================
# PART 1: ngOnChanges
# ============================================

cat > changes.ts << 'EOF'
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-child',
  standalone: true,
  template: `<p>{{ name }}</p>`
})
export class ChildComponent implements OnChanges {
  @Input() name = '';

  ngOnChanges(changes: SimpleChanges): void {
    const c = changes['name'];
    if (c) {
      console.log(`${c.previousValue} → ${c.currentValue} (first: ${c.firstChange})`);
    }
  }
}
EOF

npx tsc --noEmit changes.ts
# (no errors)

# ============================================
# PART 2: ngAfterViewInit
# ============================================

cat > view.ts << 'EOF'
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-focus',
  standalone: true,
  template: `<input #field type="text">`
})
export class FocusComponent implements AfterViewInit {
  @ViewChild('field') fieldRef!: ElementRef<HTMLInputElement>;

  ngAfterViewInit(): void {
    this.fieldRef.nativeElement.focus();
  }
}
EOF

npx tsc --noEmit view.ts
# (no errors)

# ============================================
# PART 3: ngAfterContentInit
# ============================================

cat > content.ts << 'EOF'
import { Component, ContentChild, ElementRef, AfterContentInit } from '@angular/core';

@Component({
  selector: 'app-card',
  standalone: true,
  template: `<div class="card"><ng-content></ng-content></div>`
})
export class CardComponent implements AfterContentInit {
  @ContentChild('title') titleRef?: ElementRef;

  ngAfterContentInit(): void {
    console.log('Title:', this.titleRef?.nativeElement?.textContent);
  }
}
EOF

npx tsc --noEmit content.ts
# (no errors)

# ============================================
# PART 4: ngDoCheck with differ
# ============================================

cat > check.ts << 'EOF'
import { Component, Input, DoCheck, IterableDiffers, IterableDiffer } from '@angular/core';

@Component({
  selector: 'app-list',
  standalone: true,
  template: `<p>{{ items.length }} items</p>`
})
export class ListComponent implements DoCheck {
  @Input() items: string[] = [];
  private differ: IterableDiffer<string>;

  constructor(differs: IterableDiffers) {
    this.differ = differs.find([]).create();
  }

  ngDoCheck(): void {
    const changes = this.differ.diff(this.items);
    if (changes) {
      changes.forEachAddedItem(r => console.log('+', r.item));
      changes.forEachRemovedItem(r => console.log('-', r.item));
    }
  }
}
EOF

npx tsc --noEmit check.ts
# (no errors)

# ============================================
# PART 5: FULL LIFECYCLE ORDER
# ============================================

cat > order.ts << 'EOF'
import {
  Component, Input, OnChanges, OnInit, DoCheck,
  AfterContentInit, AfterContentChecked,
  AfterViewInit, AfterViewChecked, OnDestroy, SimpleChanges
} from '@angular/core';

@Component({
  selector: 'app-lifecycle',
  standalone: true,
  template: `<p>{{ value }}</p>`
})
export class LifecycleComponent implements
  OnChanges, OnInit, DoCheck, AfterContentInit,
  AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {

  @Input() value = 0;

  ngOnChanges(c: SimpleChanges): void { console.log('ngOnChanges'); }
  ngOnInit(): void { console.log('ngOnInit'); }
  ngDoCheck(): void { console.log('ngDoCheck'); }
  ngAfterContentInit(): void { console.log('ngAfterContentInit'); }
  ngAfterContentChecked(): void { console.log('ngAfterContentChecked'); }
  ngAfterViewInit(): void { console.log('ngAfterViewInit'); }
  ngAfterViewChecked(): void { console.log('ngAfterViewChecked'); }
  ngOnDestroy(): void { console.log('ngOnDestroy'); }
}
EOF

npx tsc --noEmit order.ts
# (no errors)

# ============================================
# PART 6: HOOK AVAILABILITY
# ============================================

cat > availability.ts << 'EOF'
import { Component, OnInit, AfterViewInit, ViewChild, ElementRef, Input } from '@angular/core';

@Component({
  selector: 'app-demo',
  standalone: true,
  template: `<input #field><p>{{ label }}</p>`
})
export class DemoComponent implements OnInit, AfterViewInit {
  @Input() label = '';
  @ViewChild('field') fieldRef?: ElementRef;

  ngOnInit(): void {
    console.log('ngOnInit — label:', this.label);
    console.log('ngOnInit — field:', this.fieldRef);  // undefined
  }

  ngAfterViewInit(): void {
    console.log('ngAfterViewInit — field:', this.fieldRef?.nativeElement);
  }
}
EOF

npx tsc --noEmit availability.ts
# (no errors)

Quick Reference

Full Hook Order

OrderHook
1constructor
2ngOnChanges (first)
3ngOnInit
4ngDoCheck
5ngAfterContentInit
6ngAfterContentChecked
7ngAfterViewInit
8ngAfterViewChecked
On destroyngOnDestroy

On Every CD Cycle

HookRuns
ngOnChangesOnly if inputs changed
ngDoCheck✅ Always
ngAfterContentChecked✅ Always
ngAfterViewChecked✅ Always

Hook Frequencies

FrequencyHooks
Onceconstructor, ngOnInit, ngAfterContentInit, ngAfterViewInit, ngOnDestroy
On input changengOnChanges
Every CD cyclengDoCheck, ngAfterContentChecked, ngAfterViewChecked

What’s Available When

HookInputsContentViewDOM
constructor
ngOnChanges
ngOnInit
ngDoCheck
ngAfterContentInitPartial
ngAfterViewInit

When to Use

HookUse for
ngOnChangesReact to input changes
ngDoCheckCustom change detection
ngAfterContentInitRead projected content
ngAfterViewInitDOM work, focus, third-party init
ngAfterContentCheckedRarely — check content each cycle
ngAfterViewCheckedRarely — check view each cycle
ngOnDestroyCleanup

SimpleChanges

FieldMeaning
previousValuePrevious value
currentValueNew value
firstChangeTrue on first assignment
KeyInput name

Signal Inputs vs ngOnChanges

Aspect@Input()input()
Fires ngOnChanges
Reactive readsManual
In computed
In effect
RecommendedLegacyModern

@ViewChild vs @ContentChild

Aspect@ViewChild@ContentChild
SourceOwn templateProjected content
Ready inngAfterViewInitngAfterContentInit
Set byComponent itselfParent
Example<input #field><ng-content>

Common Errors

ErrorCause
ExpressionChangedAfterItHasBeenCheckedChanging state in checked hooks
Cannot read property of undefinedQuery used before its hook
Infinite loopState change in ngAfterViewChecked
Input undefinedRead in constructor

static: true vs static: false

OptionResolved
static: trueBefore ngOnInit — for unconditional queries
static: false (default)Before ngAfterViewInit — for conditional queries

Best Practices Summary

RuleReason
Constructor for DI onlyInputs not set
Inputs in ngOnInitAvailable there
DOM in ngAfterViewInitView exists
Content in ngAfterContentInitProjected content ready
Avoid ngDoCheckRuns on every cycle
Avoid checked hooksRuns constantly
Clean up in ngOnDestroyPrevent leaks

Best Practices

Do This:

// React to input changes in ngOnChanges
ngOnChanges(changes: SimpleChanges): void {
  if (changes['userId']) {
    this.load(changes['userId'].currentValue);
  }
}                                                          // ✅

// Use firstChange for initial vs updates
if (changes['x']?.firstChange) { /* initial */ }           // ✅

// Use ngAfterViewInit for DOM work
ngAfterViewInit(): void {
  this.inputRef.nativeElement.focus();
}                                                          // ✅

// Use ngAfterContentInit for projected content
ngAfterContentInit(): void {
  console.log(this.titleRef?.nativeElement);
}                                                          // ✅

// Prefer signals over ngOnChanges for new code
userId = input.required<number>();
effect(() => this.load(this.userId()));                    // ✅

// Use static: true for unconditional @ViewChild
@ViewChild('x', { static: true }) x!: ElementRef;          // ✅

// Clean up in ngOnDestroy
ngOnDestroy(): void { this.sub?.unsubscribe(); }           // ✅

// Defer state changes out of checked hooks
ngAfterViewInit(): void {
  setTimeout(() => { this.value = 1; });
}                                                          // ✅

Don’t Do This:

// Don't read inputs in the constructor
constructor() {
  console.log(this.userId);  // ❌ undefined                // ❌
}

// Don't access @ViewChild in ngOnInit
ngOnInit(): void {
  this.field.nativeElement.focus();  // ❌ not resolved yet  // ❌
}

// Don't do heavy work in ngDoCheck
ngDoCheck(): void {
  this.sortLargeList();  // ❌ runs every CD cycle           // ❌
}

// Don't change state in ngAfterViewChecked
ngAfterViewChecked(): void {
  this.count = Math.random();  // ❌ infinite CD loop        // ❌
}

// Don't forget to unsubscribe
ngOnInit(): void {
  setInterval(() => {}, 1000);  // ❌ leak without ngOnDestroy // ❌
}

// Don't confuse @ViewChild with @ContentChild
@ViewChild('x') // ❌ for projected content — use @ContentChild // ❌

// Don't mutate inputs and expect ngOnChanges
this.items.push(newItem);  // ⚠️  no change detected        // ⚠️

// Don't rely on ngOnChanges for signal inputs
value = input(0);
ngOnChanges(): void { }  // ⚠️  won't fire for signal input  // ⚠️

Common Pitfalls

PitfallProblemSolution
Inputs in constructorUndefinedUse ngOnInit
@ViewChild in ngOnInitUndefinedUse ngAfterViewInit
@ContentChild in ngAfterViewInitWrong hookUse ngAfterContentInit
State change in ngAfterViewCheckedInfinite loopUse setTimeout or effect
Heavy work in ngDoCheckSlow appRestructure or use signals
Mutating input arrayngOnChanges doesn’t fireImmutable update
Forgetting ngOnDestroyMemory leakAlways unsubscribe
static: true on conditional queryQuery undefinedUse static: false
Signal input with ngOnChangesNever firesUse effect
SimpleChanges assumed completeOnly changed keysCheck if (changes['x'])

Real-World Examples

1. React to input change

ngOnChanges(c: SimpleChanges): void {
  if (c['userId']) this.load(c['userId'].currentValue);
}

2. First change only

if (c['x']?.firstChange) this.init(c['x'].currentValue);

3. Previous value comparison

if (c['sort'] && c['sort'].previousValue !== c['sort'].currentValue) {
  this.resort();
}

4. Focus an input

ngAfterViewInit(): void {
  this.inputRef.nativeElement.focus();
}

5. Measure an element

ngAfterViewInit(): void {
  const el = this.boxRef.nativeElement;
  console.log(el.offsetWidth, el.offsetHeight);
}

6. Init a chart

ngAfterViewInit(): void {
  this.chart = new Chart(this.canvasRef.nativeElement, this.options);
}

7. Read projected title

ngAfterContentInit(): void {
  console.log(this.titleRef?.nativeElement.textContent);
}

8. Set tab indices

ngAfterContentInit(): void {
  this.tabs.forEach((tab, i) => tab.index = i);
}

9. Track array changes

ngDoCheck(): void {
  const changes = this.differ.diff(this.items);
  if (changes) this.handle(changes);
}

10. Scroll on init

ngAfterViewInit(): void {
  this.mainRef.nativeElement.scrollTop = 0;
}

11. Deferred state change

ngAfterViewInit(): void {
  setTimeout(() => { this.ready = true; });
}

12. Manual change detection

ngAfterViewInit(): void {
  this.cdr.detectChanges();
}

13. Unsubscribe from multiple subscriptions

private subs = new Subscription();

ngOnInit(): void {
  this.subs.add(this.a$.subscribe());
  this.subs.add(this.b$.subscribe());
}

ngOnDestroy(): void {
  this.subs.unsubscribe();
}

14. Use takeUntilDestroyed

constructor() {
  interval(1000).pipe(takeUntilDestroyed()).subscribe();
}

15. static: true for a static view child

@ViewChild('header', { static: true }) header!: ElementRef;

16. static: false for conditional view child

@ViewChild('panel', { static: false }) panel?: ElementRef;

17. Content children query

@ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;

ngAfterContentInit(): void {
  this.tabs.forEach((t, i) => t.index = i);
}

18. Set up an effect instead of ngOnChanges

userId = input.required<number>();

constructor() {
  effect(() => this.load(this.userId()));
}

19. Clean up a timer

private id?: number;

ngOnInit(): void {
  this.id = window.setInterval(() => {}, 1000);
}

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

20. Cleanup with DestroyRef

private destroyRef = inject(DestroyRef);

constructor() {
  const observer = new ResizeObserver(() => {});
  observer.observe(el);
  this.destroyRef.onDestroy(() => observer.disconnect());
}

Visual: Full Lifecycle Order

┌──────────────────────────────────────────────┐
│  1. constructor                              │
│     └── DI only, no inputs, no view          │
│                                              │
│  2. ngOnChanges (first)                      │
│     └── inputs available via changes object  │
│                                              │
│  3. ngOnInit                                 │
│     └── inputs set directly, no view         │
│                                              │
│  4. ngDoCheck                                │
│     └── starts running on every CD cycle     │
│                                              │
│  5. ngAfterContentInit                       │
│     └── projected content ready              │
│                                              │
│  6. ngAfterContentChecked                    │
│     └── runs every cycle                     │
│                                              │
│  7. ngAfterViewInit                          │
│     └── own view ready, DOM rendered         │
│                                              │
│  8. ngAfterViewChecked                       │
│     └── runs every cycle                     │
│                                              │
│  ... (later, on destroy)                     │
│                                              │
│  ngOnDestroy                                 │
│                                              │
└──────────────────────────────────────────────┘

Visual: Every Subsequent Cycle

┌──────────────────────────────────────────────┐
│  Change detection trigger                    │
│       │                                      │
│       ▼                                      │
│  ngOnChanges (if inputs changed)             │
│       │                                      │
│       ▼                                      │
│  ngDoCheck                                   │
│       │                                      │
│       ▼                                      │
│  ngAfterContentChecked                       │
│       │                                      │
│       ▼                                      │
│  ngAfterViewChecked                          │
│                                              │
└──────────────────────────────────────────────┘

Visual: What’s Available When

┌──────────────────────────────────────────────┐
│  constructor                                 │
│  ├── inputs      ❌                          │
│  ├── content     ❌                          │
│  ├── view        ❌                          │
│  └── DOM         ❌                          │
│                                              │
│  ngOnInit                                    │
│  ├── inputs      ✅                          │
│  ├── content     ❌                          │
│  ├── view        ❌                          │
│  └── DOM         ❌                          │
│                                              │
│  ngAfterContentInit                          │
│  ├── inputs      ✅                          │
│  ├── content     ✅                          │
│  ├── view        ❌                          │
│  └── DOM         Partial                     │
│                                              │
│  ngAfterViewInit                             │
│  ├── inputs      ✅                          │
│  ├── content     ✅                          │
│  ├── view        ✅                          │
│  └── DOM         ✅                          │
│                                              │
└──────────────────────────────────────────────┘

Visual: Query Resolution

┌──────────────────────────────────────────────┐
│  @ContentChild('title')                      │
│       │                                      │
│       ▼                                      │
│  Resolved before ngAfterContentInit          │
│       │                                      │
│       ▼                                      │
│  Accessible in ngAfterContentInit ✅         │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  @ViewChild('field')                         │
│       │                                      │
│       ▼                                      │
│  Resolved before ngAfterViewInit             │
│       │                                      │
│       ▼                                      │
│  Accessible in ngAfterViewInit ✅            │
│                                              │
└──────────────────────────────────────────────┘

Visual: ngOnChanges Flow

┌──────────────────────────────────────────────┐
│  Parent updates binding                      │
│       │                                      │
│       ▼                                      │
│  Angular compares previous / current         │
│       │                                      │
│       ▼                                      │
│  Different? ──► Yes ──► ngOnChanges          │
│              └► No  ──► skip                 │
│                                              │
│  Changes object:                             │
│  { key: { previousValue, currentValue, firstChange } }│
│                                              │
└──────────────────────────────────────────────┘

Visual: Infinite Loop Trap

┌──────────────────────────────────────────────┐
│  ngAfterViewChecked() {                      │
│    this.value = Math.random();               │
│  }                                           │
│                                              │
└──────────────────────────────────────────────┘
                  │
                  ▼
┌──────────────────────────────────────────────┐
│  Value changes → CD schedule new cycle       │
│       │                                      │
│       ▼                                      │
│  ngAfterViewChecked runs again               │
│       │                                      │
│       ▼                                      │
│  Value changes again → loop                  │
│       │                                      │
│       ▼                                      │
│  Angular throws after threshold              │
│                                              │
└──────────────────────────────────────────────┘

Visual: Signal Input Alternative

┌──────────────────────────────────────────────┐
│  Classic — @Input + ngOnChanges              │
│                                              │
│  @Input() userId!: number;                   │
│                                              │
│  ngOnChanges(c) {                            │
│    if (c['userId']) this.load(c['userId'].currentValue);│
│  }                                           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Modern — signal input + effect              │
│                                              │
│  userId = input.required<number>();          │
│                                              │
│  constructor() {                             │
│    effect(() => this.load(this.userId()));   │
│  }                                           │
│                                              │
│  Runs on every userId change — including first│
│                                              │
└──────────────────────────────────────────────┘

Visual: ngAfterViewInit Use Cases

┌──────────────────────────────────────────────┐
│  ngAfterViewInit                             │
│                                              │
│  ✓ Focus an input                            │
│  ✓ Measure element                           │
│  ✓ Init chart / canvas                       │
│  ✓ Call child component methods              │
│  ✓ Integrate third-party libs                │
│  ✓ Scroll to position                        │
│  ✓ Set up DOM observers                      │
│                                              │
└──────────────────────────────────────────────┘

Visual: Hook Frequency

┌──────────────────────────────────────────────┐
│  Once per instance                           │
│  ─ constructor                               │
│  ─ ngOnInit                                  │
│  ─ ngAfterContentInit                        │
│  ─ ngAfterViewInit                           │
│  ─ ngOnDestroy                               │
│                                              │
├──────────────────────────────────────────────┤
│  On input change                             │
│  ─ ngOnChanges                               │
│                                              │
├──────────────────────────────────────────────┤
│  Every CD cycle                              │
│  ─ ngDoCheck                                 │
│  ─ ngAfterContentChecked                     │
│  ─ ngAfterViewChecked                        │
│                                              │
└──────────────────────────────────────────────┘

Visual: Decision Flow

┌──────────────────────────────────────────────┐
│  React to input changes?                     │
│       └── ngOnChanges or effect              │
│                                              │
│  Set up on init?                             │
│       └── ngOnInit                           │
│                                              │
│  Access projected content?                   │
│       └── ngAfterContentInit                 │
│                                              │
│  Access DOM or view children?                │
│       └── ngAfterViewInit                    │
│                                              │
│  Custom change detection?                    │
│       └── ngDoCheck (sparingly)              │
│                                              │
│  Every CD cycle?                             │
│       └── Avoid; use computed/effect         │
│                                              │
│  Cleanup?                                    │
│       └── ngOnDestroy                        │
│                                              │
└──────────────────────────────────────────────┘

Summary

HookFrequencyUse for
constructorOnceDI only
ngOnChangesInput changeReact to input changes
ngOnInitOnceSetup, initial data
ngDoCheckEvery CDCustom change detection
ngAfterContentInitOnceRead projected content
ngAfterContentCheckedEvery CDRarely
ngAfterViewInitOnceDOM work, view children
ngAfterViewCheckedEvery CDRarely
ngOnDestroyOnceCleanup

Key takeaways:

  • The lifecycle has a fixed order — constructor, changes, init, checked, destroy
  • ngOnChanges fires on every input change with a SimpleChanges object
  • SimpleChanges has previousValue, currentValue, and firstChange
  • ngOnInit runs once after inputs are set, but before the view exists
  • ngDoCheck runs on every CD cycle — for custom detection, used sparingly
  • ngAfterContentInit runs once after projected content is ready
  • ngAfterViewInit runs once after the component’s own view is ready
  • @ContentChild queries are resolved by ngAfterContentInit
  • @ViewChild queries are resolved by ngAfterViewInit
  • “Checked” hooksngAfterContentChecked, ngAfterViewChecked — run every cycle; avoid work there
  • Changing state in checked hooks can cause infinite loops
  • Signal inputs replace ngOnChanges for reactive behavior — use effect
  • static: true resolves @ViewChild earlier; static: false (default) resolves it at ngAfterViewInit
  • Mutating inputs doesn’t fire ngOnChanges — use immutable updates

Remember: The full lifecycle has phases, and each phase exposes different state. ngOnChanges for input changes, ngOnInit for setup, ngAfterContentInit for projected content, ngAfterViewInit for the view and DOM. The “checked” hooks run constantly — keep them cheap or avoid them entirely. Modern Angular prefers signal inputs with effect over ngOnChanges, and computed signals over checked hooks. But the classic hooks remain the foundation for decorator-based components, and knowing when each fires is the difference between components that work reliably and components that work by accident.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!