Component Lifecycle Hooks
Every Angular component goes through a series of lifecycle phases — from creation to destruction. Angular calls specific methods (called lifecycle hooks) at each phase, giving you a chance to run code at the right moment.
Lifecycle hooks are interfaces you implement on the component class. When Angular detects a class implements one, it calls that method at the appropriate time.
The Complete Lifecycle Sequence
The hooks run in this order when a component is created, used, and destroyed:
Constructor() ← Not a lifecycle hook, but runs first
ngOnChanges() ← 1st: fires before ngOnInit if there are @Input() bindings
Also fires whenever an @Input() value changes
ngOnInit() ← 2nd: fires once after the first ngOnChanges
ngDoCheck() ← 3rd: fires on every change detection cycle (use carefully)
ngAfterContentInit() ← 4th: fires once after ng-content is initialized
ngAfterContentChecked() ← 5th: fires after every ng-content check
ngAfterViewInit() ← 6th: fires once after the component's view (and child views) are initialized
ngAfterViewChecked() ← 7th: fires after every view check
ngOnDestroy() ← Last: fires just before the component is removed from the DOMLifecycle Hooks Quick Reference
Hook | Interface | When it fires | Common Use |
|---|---|---|---|
ngOnChanges | OnChanges | Before ngOnInit and on every @Input change | React to input changes, validate inputs |
ngOnInit | OnInit | Once, after first ngOnChanges | Fetch initial data, setup subscriptions |
ngDoCheck | DoCheck | Every change detection run | Custom change detection (rarely needed) |
ngAfterContentInit | AfterContentInit | Once, after ng-content projected | Access @ContentChild queries |
ngAfterContentChecked | AfterContentChecked | After every content check | Respond to projected content changes |
ngAfterViewInit | AfterViewInit | Once, after component view initialized | Access @ViewChild DOM elements |
ngAfterViewChecked | AfterViewChecked | After every view check | Respond to view changes (rare) |
ngOnDestroy | OnDestroy | Just before component is destroyed | Unsubscribe, clean up timers, close connections |
ngOnInit — The Most Important Hook
ngOnInit is the hook you will use most often. It runs once, after all @Input values have been set. Use it for:
- Fetching initial data from a service
- Setting up subscriptions
- Initialization logic that depends on
@Inputvalues
import { Component, Input, OnInit, inject } from '@angular/core';
import { ProductService } from '../services/product.service';
interface Product { id: number; name: string; price: number; }
@Component({
selector: 'app-product-detail',
standalone: true,
template: `
@if (product) {
<h2>{{ product.name }}</h2>
<p>Price: {{ product.price | currency }}</p>
} @else {
<p>Loading...</p>
}
`,
})
export class ProductDetailComponent implements OnInit {
@Input({ required: true }) productId!: number;
product: Product | null = null;
private productService = inject(ProductService);
ngOnInit(): void {
// Safe to use this.productId here — it has been set by Angular
this.productService.getById(this.productId).subscribe(p => {
this.product = p;
});
}
}@Input() values in the constructor. The constructor runs before inputs are set. Use ngOnInit for initialization code.ngOnChanges — Reacting to Input Changes
ngOnChanges fires before ngOnInit and every time an @Input value changes. It receives a SimpleChanges object with the previous and current values:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-chart',
standalone: true,
template: `<canvas #chart></canvas>`,
})
export class ChartComponent implements OnChanges {
@Input({ required: true }) data!: number[];
@Input() color = 'blue';
ngOnChanges(changes: SimpleChanges): void {
console.log('Changes:', changes);
// changes.data — { currentValue, previousValue, firstChange, isFirstChange() }
// changes.color — { currentValue, previousValue, firstChange, isFirstChange() }
if (changes['data']) {
const prev = changes['data'].previousValue;
const curr = changes['data'].currentValue;
const isFirst = changes['data'].firstChange;
if (!isFirst) {
console.log(`Data changed from ${prev} to ${curr}`);
}
this.redrawChart();
}
}
private redrawChart(): void {
// Re-render the chart when data changes
}
}ngOnChanges only fires for reference changes to object and array inputs — mutating an array in place will not trigger it. Assign a new array reference: this.items = [...this.items, newItem].ngOnDestroy — Cleaning Up
ngOnDestroy fires just before Angular removes the component from the DOM. This is where you must clean up:
- Unsubscribe from Observables (memory leak prevention)
- Clear
setTimeout/setInterval - Close WebSocket connections
- Remove event listeners added with native DOM APIs
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-live-clock',
standalone: true,
template: `<p>{{ currentTime }}</p>`,
})
export class LiveClockComponent implements OnInit, OnDestroy {
currentTime = '';
private subscription!: Subscription;
private timerId!: ReturnType<typeof setInterval>;
ngOnInit(): void {
// Subscribe to an interval observable
this.subscription = interval(1000).subscribe(() => {
this.currentTime = new Date().toLocaleTimeString();
});
// Or use a native timer
this.timerId = setInterval(() => {
this.currentTime = new Date().toLocaleTimeString();
}, 1000);
}
ngOnDestroy(): void {
// Clean up — prevent memory leaks
this.subscription?.unsubscribe();
clearInterval(this.timerId);
}
}takeUntilDestroyed() operator (Angular 16+). It automatically unsubscribes when the component is destroyed, without needing ngOnDestroy.import { Component, OnInit, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({
selector: 'app-live-clock',
standalone: true,
template: `<p>{{ currentTime }}</p>`,
})
export class LiveClockComponent implements OnInit {
currentTime = '';
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
interval(1000)
.pipe(takeUntilDestroyed(this.destroyRef)) // auto-unsubscribes!
.subscribe(() => {
this.currentTime = new Date().toLocaleTimeString();
});
}
}ngAfterViewInit — Accessing the DOM
ngAfterViewInit fires after Angular has fully initialized the component's view, including all child component views. This is the earliest safe moment to access @ViewChild references:
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-auto-focus',
standalone: true,
template: `
<input #searchInput type="text" placeholder="Search..." />
`,
})
export class AutoFocusComponent implements AfterViewInit {
@ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;
ngAfterViewInit(): void {
// Safe to access the DOM element here
this.searchInput.nativeElement.focus();
}
}@ViewChild in ngOnInit — the view has not been initialized yet at that point. Always use ngAfterViewInit for the first access.ngAfterContentInit — Content Projection
ngAfterContentInit fires after Angular projects external content into the component via <ng-content>. Use it to access @ContentChild and @ContentChildren queries:
import { Component, ContentChild, AfterContentInit, ElementRef } from '@angular/core';
@Component({
selector: 'app-panel',
standalone: true,
template: `
<div class="panel">
<div class="panel-body">
<ng-content /> <!-- projected content goes here -->
</div>
</div>
`,
})
export class PanelComponent implements AfterContentInit {
@ContentChild('panelTitle') titleEl!: ElementRef;
ngAfterContentInit(): void {
// The projected content is now available
if (this.titleEl) {
console.log('Panel title:', this.titleEl.nativeElement.textContent);
}
}
}Complete Lifecycle Example
import {
Component, Input, OnChanges, OnInit, DoCheck,
AfterContentInit, AfterContentChecked, AfterViewInit,
AfterViewChecked, OnDestroy, SimpleChanges
} from '@angular/core';
@Component({
selector: 'app-lifecycle-demo',
standalone: true,
template: `<p>{{ name }}</p>`,
})
export class LifecycleDemoComponent
implements OnChanges, OnInit, DoCheck,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy
{
@Input() name = '';
constructor() {
console.log('1. constructor — inputs NOT available yet');
}
ngOnChanges(changes: SimpleChanges) {
console.log('2. ngOnChanges — input changed:', changes);
}
ngOnInit() {
console.log('3. ngOnInit — first initialization, inputs available');
}
ngDoCheck() {
console.log('4. ngDoCheck — custom change detection');
}
ngAfterContentInit() {
console.log('5. ngAfterContentInit — ng-content initialized');
}
ngAfterContentChecked() {
console.log('6. ngAfterContentChecked — ng-content checked');
}
ngAfterViewInit() {
console.log('7. ngAfterViewInit — view and child views initialized');
}
ngAfterViewChecked() {
console.log('8. ngAfterViewChecked — view and child views checked');
}
ngOnDestroy() {
console.log('9. ngOnDestroy — cleanup before destruction');
}
}Choosing the Right Hook
Task | Use This Hook |
|---|---|
Fetch data from a service | ngOnInit |
React when an @Input changes after init | ngOnChanges or signal effect() |
Access @ViewChild DOM elements | ngAfterViewInit |
Access @ContentChild projected content | ngAfterContentInit |
Unsubscribe / clean up | ngOnDestroy (or takeUntilDestroyed) |
Set up initial state (no @Input needed) | Constructor or ngOnInit |
Manual change detection | ngDoCheck (rarely needed) |
Lifecycle Hooks with Signals
When using Angular Signals, many lifecycle hook patterns become simpler. Signals automatically track their dependencies and update consumers without manual subscription management:
import { Component, Input, OnInit, signal, effect, inject } from '@angular/core';
import { DataService } from '../services/data.service';
@Component({
selector: 'app-signal-demo',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
`,
})
export class SignalDemoComponent implements OnInit {
@Input() initialCount = 0;
count = signal(0);
// computed signals auto-update — no ngDoCheck needed
double = signal(0);
private dataService = inject(DataService);
constructor() {
// effects run whenever their signals change — no OnChanges needed
effect(() => {
console.log('Count changed to:', this.count());
this.double.set(this.count() * 2);
});
}
ngOnInit() {
this.count.set(this.initialCount);
}
}ngOnChanges and ngDoCheck much less often. Signal effect() and computed() replace most reactive patterns that previously required these hooks.