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:
| Hook | Runs |
|---|---|
constructor | Once, before inputs |
ngOnChanges | First time and on every input change |
ngOnInit | Once, after first ngOnChanges |
ngDoCheck | Every CD cycle |
ngAfterContentInit | Once, after content initializes |
ngAfterContentChecked | Every CD cycle after content checked |
ngAfterViewInit | Once, after view initializes |
ngAfterViewChecked | Every CD cycle after view checked |
ngOnDestroy | Once, 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.
ngAfterViewInitis where@ViewChildqueries are resolved.ngOnChangesis 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
effectorcomputedinstead)
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 inngOnChanges; useeffectorcomputedinstead - Values set directly on the component instance (not via binding)
- Same reference — if an object is mutated in place,
ngOnChangesdoesn’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 acomputedoreffectre-runs when they change. That’s more ergonomic thanngOnChangeswith itsSimpleChangesobject. New code should prefer signal inputs for reactive behavior;ngOnChangesremains 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.
ngDoCheckruns on every CD cycle — potentially dozens of times per second. Expensive work here destroys performance. - Prefer
computedsignals 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
ngDoCheckis a last resort: Every other hook has a specific trigger.ngDoCheckruns 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.ngDoCheckis 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
ngDoCheckon the first cycle - After projected content is initialized
- Before the component’s own view is initialized
What’s available:
@ContentChildand@ContentChildrenqueries are resolved- The projected content’s directives and components exist
What’s not available:
- The component’s own view —
@ViewChildqueries 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.
ngAfterContentInithandles the first,ngAfterViewInitthe 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
ngAfterContentInitandngAfterContentChecked - After the component’s view — including children — is initialized
- Before
ngAfterViewChecked
What’s available:
@ViewChildand@ViewChildrenqueries 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. UsesetTimeoutorChangeDetectorRef.detectChanges()if you must. - Assuming
@ViewChildis 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:ngOnInitruns before Angular has rendered the component’s template. The view — its child components, its DOM nodes — doesn’t exist.ngAfterViewInitruns 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
ngAfterViewCheckedwith a manualChangeDetectorRef.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.
| Hook | Inputs 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 reads | Manual | ✅ In computed/effect |
Available in ngOnInit | ✅ | ✅ |
| Recommended | Legacy | Modern |
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.
ngOnInitruns after inputs are set. That’s why inputs are available there and not in the constructor — the same reasonngOnChangesfires beforengOnInit.
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:
- constructor — DI only, no inputs
- ngOnChanges — inputs available; label and items change
- ngOnInit — recalculates total
- ngDoCheck — checks for mutations
- ngAfterContentInit — projected title available
- ngAfterViewInit — main element exists; scroll set
- Later, on Add —
ngOnChangesfires again, thenngDoCheck,ngAfterContentChecked,ngAfterViewChecked - 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.
ngOnChangesreads inputs,ngOnInitcomputes,ngDoChecktracks mutations,ngAfterContentInitreads projected content,ngAfterViewInittouches the DOM,ngOnDestroycleans 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
| Order | Hook |
|---|---|
| 1 | constructor |
| 2 | ngOnChanges (first) |
| 3 | ngOnInit |
| 4 | ngDoCheck |
| 5 | ngAfterContentInit |
| 6 | ngAfterContentChecked |
| 7 | ngAfterViewInit |
| 8 | ngAfterViewChecked |
| On destroy | ngOnDestroy |
On Every CD Cycle
| Hook | Runs |
|---|---|
ngOnChanges | Only if inputs changed |
ngDoCheck | ✅ Always |
ngAfterContentChecked | ✅ Always |
ngAfterViewChecked | ✅ Always |
Hook Frequencies
| Frequency | Hooks |
|---|---|
| Once | constructor, ngOnInit, ngAfterContentInit, ngAfterViewInit, ngOnDestroy |
| On input change | ngOnChanges |
| Every CD cycle | ngDoCheck, ngAfterContentChecked, ngAfterViewChecked |
What’s Available When
| Hook | Inputs | Content | View | DOM |
|---|---|---|---|---|
constructor | ❌ | ❌ | ❌ | ❌ |
ngOnChanges | ✅ | ❌ | ❌ | ❌ |
ngOnInit | ✅ | ❌ | ❌ | ❌ |
ngDoCheck | ✅ | ❌ | ❌ | ❌ |
ngAfterContentInit | ✅ | ✅ | ❌ | Partial |
ngAfterViewInit | ✅ | ✅ | ✅ | ✅ |
When to Use
| Hook | Use for |
|---|---|
ngOnChanges | React to input changes |
ngDoCheck | Custom change detection |
ngAfterContentInit | Read projected content |
ngAfterViewInit | DOM work, focus, third-party init |
ngAfterContentChecked | Rarely — check content each cycle |
ngAfterViewChecked | Rarely — check view each cycle |
ngOnDestroy | Cleanup |
SimpleChanges
| Field | Meaning |
|---|---|
previousValue | Previous value |
currentValue | New value |
firstChange | True on first assignment |
| Key | Input name |
Signal Inputs vs ngOnChanges
| Aspect | @Input() | input() |
|---|---|---|
Fires ngOnChanges | ✅ | ❌ |
| Reactive reads | Manual | ✅ |
In computed | ❌ | ✅ |
In effect | ❌ | ✅ |
| Recommended | Legacy | Modern |
@ViewChild vs @ContentChild
| Aspect | @ViewChild | @ContentChild |
|---|---|---|
| Source | Own template | Projected content |
| Ready in | ngAfterViewInit | ngAfterContentInit |
| Set by | Component itself | Parent |
| Example | <input #field> | <ng-content> |
Common Errors
| Error | Cause |
|---|---|
ExpressionChangedAfterItHasBeenChecked | Changing state in checked hooks |
Cannot read property of undefined | Query used before its hook |
| Infinite loop | State change in ngAfterViewChecked |
| Input undefined | Read in constructor |
static: true vs static: false
| Option | Resolved |
|---|---|
static: true | Before ngOnInit — for unconditional queries |
static: false (default) | Before ngAfterViewInit — for conditional queries |
Best Practices Summary
| Rule | Reason |
|---|---|
| Constructor for DI only | Inputs not set |
Inputs in ngOnInit | Available there |
DOM in ngAfterViewInit | View exists |
Content in ngAfterContentInit | Projected content ready |
Avoid ngDoCheck | Runs on every cycle |
| Avoid checked hooks | Runs constantly |
Clean up in ngOnDestroy | Prevent 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
| Pitfall | Problem | Solution |
|---|---|---|
| Inputs in constructor | Undefined | Use ngOnInit |
@ViewChild in ngOnInit | Undefined | Use ngAfterViewInit |
@ContentChild in ngAfterViewInit | Wrong hook | Use ngAfterContentInit |
State change in ngAfterViewChecked | Infinite loop | Use setTimeout or effect |
Heavy work in ngDoCheck | Slow app | Restructure or use signals |
| Mutating input array | ngOnChanges doesn’t fire | Immutable update |
Forgetting ngOnDestroy | Memory leak | Always unsubscribe |
static: true on conditional query | Query undefined | Use static: false |
Signal input with ngOnChanges | Never fires | Use effect |
SimpleChanges assumed complete | Only changed keys | Check 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
| Hook | Frequency | Use for |
|---|---|---|
constructor | Once | DI only |
ngOnChanges | Input change | React to input changes |
ngOnInit | Once | Setup, initial data |
ngDoCheck | Every CD | Custom change detection |
ngAfterContentInit | Once | Read projected content |
ngAfterContentChecked | Every CD | Rarely |
ngAfterViewInit | Once | DOM work, view children |
ngAfterViewChecked | Every CD | Rarely |
ngOnDestroy | Once | Cleanup |
Key takeaways:
- The lifecycle has a fixed order — constructor, changes, init, checked, destroy
ngOnChangesfires on every input change with aSimpleChangesobjectSimpleChangeshaspreviousValue,currentValue, andfirstChangengOnInitruns once after inputs are set, but before the view existsngDoCheckruns on every CD cycle — for custom detection, used sparinglyngAfterContentInitruns once after projected content is readyngAfterViewInitruns once after the component’s own view is ready@ContentChildqueries are resolved byngAfterContentInit@ViewChildqueries are resolved byngAfterViewInit- “Checked” hooks —
ngAfterContentChecked,ngAfterViewChecked— run every cycle; avoid work there - Changing state in checked hooks can cause infinite loops
- Signal inputs replace
ngOnChangesfor reactive behavior — useeffect static: trueresolves@ViewChildearlier;static: false(default) resolves it atngAfterViewInit- 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!