1. Complete Lifecycle Hooks Flow
graph TD
A[Component Class Instantiated] --> B[Constructor]
B --> C[Input Properties Assigned]
C --> D[ngOnChanges - First Call]
D --> E[ngOnInit]
E --> F[ngDoCheck]
F --> G[ngAfterContentInit]
G --> H[ngAfterContentChecked]
H --> I[ngAfterViewInit]
I --> J[ngAfterViewChecked]
J --> K[Component Ready]
K --> L{Change Detection?}
L -->|Input Changed| M[ngOnChanges]
L -->|Any Change| N[ngDoCheck]
M --> O[ngAfterContentChecked]
N --> O
O --> P[ngAfterViewChecked]
P --> L
K --> Q{Destroy Component?}
Q -->|Yes| R[ngOnDestroy]
R --> S[Component Destroyed]
Q -->|No| L
๐ก Key Point: Lifecycle hooks execute in a specific order. Understanding this order is crucial for
proper component initialization and cleanup.
2. Lifecycle Hooks Quick Reference
| Hook |
Purpose |
Timing |
Usage |
| constructor() |
Dependency injection, simple initializations |
First - when class instantiated |
DI only, avoid complex logic |
| ngOnChanges() |
Respond to input property changes |
Before ngOnInit & when inputs change |
Input validation, react to changes |
| ngOnInit() |
Component initialization after inputs set |
Once, after first ngOnChanges |
Fetch data, setup subscriptions |
| ngDoCheck() |
Custom change detection |
Every change detection cycle |
Manual change detection logic |
| ngAfterContentInit() |
After content projection |
Once, after content initialized |
Access @ContentChild queries |
| ngAfterContentChecked() |
After content checked |
After every change detection |
Respond to content changes |
| ngAfterViewInit() |
After view initialized |
Once, after view ready |
Access @ViewChild, DOM manipulation |
| ngAfterViewChecked() |
After view checked |
After every change detection |
Respond to view changes |
| ngOnDestroy() |
Cleanup before destruction |
Once, before removal |
Unsubscribe, clear timers |
3. ngOnChanges() - Input Property Changes
graph TD
A[Parent Component] --> B[Passes Input to Child]
B --> C{First Time?}
C -->|Yes| D[ngOnChanges Called]
C -->|No| E{Input Value Changed?}
E -->|Yes| F[ngOnChanges Called]
E -->|No| G[No ngOnChanges]
D --> H[SimpleChanges Object]
F --> H
H --> I[Contains previousValue]
H --> J[Contains currentValue]
H --> K[Contains firstChange flag]
I --> L[Process Change]
J --> L
K --> L
L --> M[Component Updates]
Implementation Example:
@Component({
selector: 'app-user-detail',
template: `
<div class="user-card">
<h3>{{ user?.name }}</h3>
<p>Age: {{ user?.age }}</p>
<p>Status: {{ status }}</p>
</div>
`
})
export class UserDetailComponent implements
OnChanges {
@Input() user: User;
@Input() status: string;
changeLog: string[] = [];
ngOnChanges(changes: SimpleChanges): void {
for (const propName in
changes) {
const change = changes[propName];
const current = JSON.stringify(change.currentValue);
const previous = JSON.stringify(change.previousValue);
if (change.firstChange) {
console.log(`Initial value of ${propName} set to ${current}`);
} else {
console.log(`${propName} changed from ${previous} to ${current}`);
}
if (propName === 'user' && !change.firstChange) {
this.validateUser(change.currentValue);
}
}
}
private validateUser(user: User): void
{
if (user.age < 0) {
console.warn('Invalid age detected!');
}
}
}
โ ๏ธ Important: ngOnChanges only detects changes to primitive input values and object references.
It won't detect mutations within objects or arrays.
4. ngOnInit() - Component Initialization
graph TD
A[Component Constructor] --> B[DI Complete]
B --> C[Input Properties Set]
C --> D[First ngOnChanges]
D --> E[ngOnInit Called Once]
E --> F[Initialize Component State]
E --> G[Fetch Initial Data]
E --> H[Setup Subscriptions]
E --> I[Initialize Forms]
F --> J[Component Ready]
G --> J
H --> J
I --> J
J --> K[Continue Lifecycle]
Implementation Example:
@Component({
selector: 'app-user-list',
template: `
<div class="user-list">
@if (loading) {
<div>Loading...</div>
}
@for (user of users; track user.email) {
<div>{{ user.name }} - {{ user.email }}</div>
}
</div>
`
})
export class UserListComponent implements
OnInit, OnDestroy {
@Input() department: string;
users: User[] = [];
loading = false;
private subscription: Subscription;
constructor(
private userService: UserService,
private http: HttpClient,
private route: ActivatedRoute
) {
console.log('Constructor: department is', this.department);
}
ngOnInit(): void {
console.log('ngOnInit: department is', this.department);
const id = this.route.snapshot.paramMap.get('id');
this.loading = true;
this.loadUsers();
this.subscription = this.userService.userUpdates$
.subscribe(update => {
console.log('User update received:', update);
this.handleUserUpdate(update);
});
}
private loadUsers(): void {
this.userService.getUsers(this.department)
.subscribe({
next: (users) => {
this.users = users;
this.loading = false;
},
error: (error) => {
console.error('Failed to load users:', error);
this.loading = false;
}
});
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
โ
Best Practice: Use ngOnInit for component initialization logic, not the constructor. The
constructor should only be used for dependency injection.
5. ngDoCheck() - Custom Change Detection
graph TD
A[Change Detection Cycle] --> B[ngDoCheck Called]
B --> C{Custom Check Needed?}
C -->|Yes| D[Check for Changes]
D --> E{Changes Detected?}
E -->|Yes| F[Update Component State]
E -->|No| G[Skip Update]
F --> H[Trigger Updates]
G --> I[Continue Lifecycle]
H --> I
I --> J[ngAfterContentChecked]
J --> K[ngAfterViewChecked]
B --> L[โ ๏ธ Called Very Frequently]
L --> M[Performance Impact]
Implementation Example:
@Component({
selector: 'app-differ-check',
template: `
<div>
<h3>Array Changes: {{ arrayChangeCount }}</h3>
@for (item of items; track item.name) {
<div>{{ item.name }} - {{ item.value }}</div>
}
</div>
`
})
export class DifferCheckComponent implements
DoCheck, OnInit {
@Input() items: any[] = [];
arrayChangeCount = 0;
private differ: IterableDiffer<any>;
private oldLength = 0;
constructor(private iterableDiffers:
IterableDiffers) {}
ngOnInit(): void {
this.differ = this.iterableDiffers.find(this.items).create();
}
ngDoCheck(): void {
if (this.differ) {
const changes = this.differ.diff(this.items);
if (changes) {
this.arrayChangeCount++;
changes.forEachAddedItem(record => {
console.log('Added:', record.item);
});
changes.forEachRemovedItem(record => {
console.log('Removed:', record.item);
});
}
}
if (this.items.length !== this.oldLength) {
console.log(`Length changed from ${this.oldLength} to ${this.items.length}`);
this.oldLength = this.items.length;
}
}
}
๐จ Performance Warning: ngDoCheck is called very frequently - on every change detection cycle.
Avoid heavy computations here or your app will become sluggish!
6. Content Projection Hooks
graph TD
A[Parent Component Template] --> B[ng-content Projection]
B --> C[Child Component]
C --> D[ngAfterContentInit]
D --> E[@ContentChild Available]
D --> F[@ContentChildren Available]
E --> G[Access Projected Content]
F --> G
G --> H[Component Renders]
H --> I[Change Detection]
I --> J[ngAfterContentChecked]
J --> K{Content Changed?}
K -->|Yes| L[Handle Change]
K -->|No| M[Skip]
L --> I
M --> N[Continue]
Implementation Example:
@Component({
selector: 'app-parent',
template: `
<app-tabs>
<app-tab title="First" #firstTab>First Content</app-tab>
<app-tab title="Second">Second Content</app-tab>
<app-tab title="Third">Third Content</app-tab>
</app-tabs>
`
})
export class ParentComponent {}
@Component({
selector: 'app-tab',
template: `<div><ng-content></ng-content></div>`
})
export class TabComponent {
@Input() title: string;
active = false;
}
@Component({
selector: 'app-tabs',
template: `
<div class="tabs">
<div class="tab-headers">
@for (tab of tabs; track tab) {
<button (click)="selectTab(tab)" [class.active]="tab.active">
{{ tab.title }}
</button>
}
</div>
<div class="tab-content">
<ng-content></ng-content>
</div>
</div>
`
})
export class TabsComponent implements
AfterContentInit, AfterContentChecked {
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
ngAfterContentInit(): void {
console.log(`Found ${this.tabs.length} tabs`);
if (this.tabs.length > 0) {
this.selectTab(this.tabs.first);
}
this.tabs.changes.subscribe(() => {
console.log('Tabs changed!');
});
}
ngAfterContentChecked(): void {
const activeTabs = this.tabs.filter(tab => tab.active);
if (activeTabs.length === 0 && this.tabs.length > 0) {
this.selectTab(this.tabs.first);
}
}
selectTab(tab: TabComponent): void {
this.tabs.forEach(t => t.active = false);
tab.active = true;
}
}
7. View Initialization Hooks
graph TD
A[Component Template Rendered] --> B[View Created]
B --> C[ngAfterViewInit]
C --> D[@ViewChild Available]
C --> E[@ViewChildren Available]
C --> F[DOM Elements Ready]
D --> G[Access Child Components]
E --> G
F --> H[DOM Manipulation Safe]
G --> I[Initialize Third-party Libs]
H --> I
I --> J[View Complete]
J --> K[Change Detection]
K --> L[ngAfterViewChecked]
L --> M{View Changed?}
M -->|Yes| N[Handle View Change]
M -->|No| O[Continue]
N --> K
Implementation Example:
@Component({
selector: 'app-chart',
template: `
<div class="chart-container">
<canvas #chartCanvas></canvas>
<app-legend #legend [data]="chartData"></app-legend>
</div>
<button (click)="updateChart()">Update</button>
`
})
export class ChartComponent implements
AfterViewInit, AfterViewChecked, OnDestroy {
@ViewChild('chartCanvas', { static: false })
canvas: ElementRef<HTMLCanvasElement>;
@ViewChild(LegendComponent)
legend: LegendComponent;
chartData = [];
private chart: any;
private resizeObserver: ResizeObserver;
ngAfterViewInit(): void {
console.log('Canvas element:', this.canvas.nativeElement);
console.log('Legend component:', this.legend);
this.initializeChart();
this.resizeObserver = new ResizeObserver(entries => {
this.handleResize(entries[0].contentRect);
});
this.resizeObserver.observe(this.canvas.nativeElement);
this.legend.highlightFirst();
}
ngAfterViewChecked(): void {
if (this.shouldAdjustLayout())
{
this.adjustChartLayout();
}
}
private initializeChart(): void {
const ctx = this.canvas.nativeElement.getContext('2d');
this.chart = new Chart(ctx, {
type: 'line',
data: this.chartData,
options: { responsive: true }
});
}
updateChart(): void {
this.chartData = [...this.chartData, Math.random() *
100];
if (this.chart) {
this.chart.update();
}
}
ngOnDestroy(): void {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.chart) {
this.chart.destroy();
}
}
}
โ ๏ธ ExpressionChangedAfterItHasBeenCheckedError: Don't change component properties that affect the
template in ngAfterViewInit or ngAfterViewChecked. This causes the infamous "Expression has changed" error!
8. ngOnDestroy() - Component Cleanup
graph TD
A[Component Removal Triggered] --> B[ngOnDestroy Called]
B --> C[Unsubscribe Observables]
B --> D[Clear Timers/Intervals]
B --> E[Disconnect Event Listeners]
B --> F[Clean External Resources]
B --> G[Cancel HTTP Requests]
C --> H[Prevent Memory Leaks]
D --> H
E --> H
F --> H
G --> H
H --> I[Component Destroyed]
I --> J[Removed from DOM]
J --> K[Garbage Collection]
Complete Cleanup Example:
@Component({
selector: 'app-dashboard',
template: `...`
})
export class DashboardComponent implements
OnInit, OnDestroy {
private dataSubscription: Subscription;
private routeSubscription: Subscription;
private destroy$ = new
Subject<void>();
private refreshTimer: any;
private animationFrame: number;
private resizeHandler = this.onResize.bind(this);
private documentClickHandler = this.onDocumentClick.bind(this);
private websocket: WebSocket;
private worker: Worker;
constructor(
private dataService: DataService,
private route: ActivatedRoute
) {}
ngOnInit(): void {
this.dataSubscription = this.dataService.getData()
.subscribe(data => console.log(data));
this.dataService.updates$
.pipe(takeUntil(this.destroy$))
.subscribe(update => console.log(update));
this.routeSubscription = this.route.params
.subscribe(params => console.log(params));
this.refreshTimer = setInterval(() => {
this.refreshData();
}, 5000);
window.addEventListener('resize', this.resizeHandler);
document.addEventListener('click', this.documentClickHandler);
this.websocket = new WebSocket('ws://localhost:8080');
if (typeof Worker !== 'undefined') {
this.worker = new Worker(new URL('./data.worker', import.meta.url));
this.worker.onmessage = ({ data }) => {
console.log(data);
};
}
this.animate();
}
ngOnDestroy(): void {
console.log('Cleaning up component resources...');
if (this.dataSubscription) {
this.dataSubscription.unsubscribe();
}
if (this.routeSubscription) {
this.routeSubscription.unsubscribe();
}
this.destroy$.next();
this.destroy$.complete();
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
}
window.removeEventListener('resize', this.resizeHandler);
document.removeEventListener('click', this.documentClickHandler);
if (this.websocket) {
this.websocket.close();
}
if (this.worker) {
this.worker.terminate();
}
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
}
this.cleanupResources();
}
private animate(): void {
this.animationFrame = requestAnimationFrame(() => {
this.animate();
});
}
private onResize(event: Event): void
{
console.log('Window resized');
}
private onDocumentClick(event: MouseEvent): void {
console.log('Document clicked');
}
}
โ
Memory Leak Prevention: Always clean up in ngOnDestroy! Unsubscribe from observables, clear
timers, remove event listeners, and close connections to prevent memory leaks.
9. Common Lifecycle Patterns
Pattern 1: Safe Subscription Management
export class SafeComponent implements
OnInit, OnDestroy {
private destroy$ = new
Subject<void>();
ngOnInit(): void {
this.service.data$
.pipe(
takeUntil(this.destroy$),
filter(data => data !== null),
map(data => data.value)
)
.subscribe(value => console.log(value));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
Pattern 2: Loading Data with Error Handling
export class DataComponent implements
OnInit {
data$: Observable<Data>;
loading = true;
error: string | null =
null;
ngOnInit(): void {
this.data$ = this.service.loadData().pipe(
tap(() => this.loading = false),
retry(2),
catchError(err => {
this.error = err.message;
this.loading = false;
return EMPTY;
}),
shareReplay(1)
);
}
}
Pattern 3: Parent-Child Communication
export class ParentComponent implements
AfterViewInit {
@ViewChild(ChildComponent) child: ChildComponent;
ngAfterViewInit(): void {
this.child.doSomething();
}
}
export class ChildComponent implements
OnChanges {
@Input() data: any;
@Output() dataChange = new EventEmitter();
ngOnChanges(changes: SimpleChanges): void {
if (changes['data'] && !changes['data'].firstChange)
{
this.processDataChange(changes['data'].currentValue);
}
}
}
10. Complete Lifecycle Execution Order
graph TD
subgraph "Initialization Phase"
A[1. Constructor] --> B[2. ngOnChanges - First]
B --> C[3. ngOnInit]
C --> D[4. ngDoCheck - First]
D --> E[5. ngAfterContentInit]
E --> F[6. ngAfterContentChecked]
F --> G[7. ngAfterViewInit]
G --> H[8. ngAfterViewChecked]
end
subgraph "Update Phase - Repeats"
H --> I[Input Change or Event]
I --> J[9. ngOnChanges - If Input]
J --> K[10. ngDoCheck]
K --> L[11. ngAfterContentChecked]
L --> M[12. ngAfterViewChecked]
M --> I
end
subgraph "Destruction Phase"
I --> N[Component Removal]
N --> O[13. ngOnDestroy]
end
Real Component Example with All Hooks:
@Component({
selector: 'app-complete-lifecycle',
template: `
<div>
<h3>{{ title }}</h3>
<ng-content></ng-content>
<div #viewChild>View Child Content</div>
</div>
`
})
export class CompleteLifecycleComponent implements
OnChanges, OnInit, DoCheck,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
@Input() title: string;
@ContentChild('content') content: ElementRef;
@ViewChild('viewChild') viewChild:
ElementRef;
private counter = 0;
constructor() {
console.log(`1. constructor - counter: ${++this.counter}`);
console.log(' - DI ready, inputs NOT set');
}
ngOnChanges(changes: SimpleChanges): void {
console.log(`2. ngOnChanges - counter: ${++this.counter}`);
console.log(' - Input properties changed:', changes);
}
ngOnInit(): void {
console.log(`3. ngOnInit - counter: ${++this.counter}`);
console.log(' - Component initialization, inputs ARE set');
console.log(` - title: ${this.title}`);
}
ngDoCheck(): void {
console.log(`4. ngDoCheck - counter: ${++this.counter}`);
console.log(' - Custom change detection');
}
ngAfterContentInit(): void {
console.log(`5. ngAfterContentInit - counter: ${++this.counter}`);
console.log(' - Content projection complete');
console.log(' - ContentChild available:', this.content);
}
ngAfterContentChecked(): void {
console.log(`6. ngAfterContentChecked - counter: ${++this.counter}`);
console.log(' - Content checked');
}
ngAfterViewInit(): void {
console.log(`7. ngAfterViewInit - counter: ${++this.counter}`);
console.log(' - View initialization complete');
console.log(' - ViewChild available:', this.viewChild);
}
ngAfterViewChecked(): void {
console.log(`8. ngAfterViewChecked - counter: ${++this.counter}`);
console.log(' - View checked');
}
ngOnDestroy(): void {
console.log(`9. ngOnDestroy - counter: ${++this.counter}`);
console.log(' - Cleanup before destruction');
}
}
11. Common Lifecycle Mistakes to Avoid
โ Mistake 1: Complex Logic in Constructor
constructor(private http:
HttpClient) {
this.http.get('/api/data').subscribe();
console.log(this.inputValue);
}
ngOnInit(): void {
this.http.get('/api/data').subscribe();
console.log(this.inputValue);
}
โ Mistake 2: Memory Leaks from Subscriptions
ngOnInit(): void {
this.service.data$.subscribe(data => {
this.data = data;
});
}
private subscription: Subscription;
ngOnInit(): void {
this.subscription = this.service.data$.subscribe(data
=> {
this.data = data;
});
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
โ Mistake 3: Changing Data in Wrong Hooks
ngAfterViewInit(): void {
this.title = 'New Title';
}
ngAfterViewInit(): void {
setTimeout(() => {
this.title = 'New Title';
});
Promise.resolve().then(() => {
this.title = 'New Title';
});
}
โ Mistake 4: Heavy Operations in Frequently Called Hooks
ngDoCheck(): void {
this.expensiveCalculation = this.items.reduce((sum,
item) => {
return sum + this.complexComputation(item);
}, 0);
}
private lastItemsLength = 0;
private cachedResult = 0;
ngDoCheck(): void {
if (this.items.length !== this.lastItemsLength) {
this.lastItemsLength = this.items.length;
this.cachedResult = this.calculateExpensive();
}
}
โ Mistake 5: Accessing View/Content Children Too Early
ngOnInit(): void {
this.viewChild.doSomething();
}
ngAfterViewInit(): void {
this.viewChild.doSomething();
}
ngAfterContentInit(): void {
this.contentChild.doSomething();
}
12. Performance Optimization Tips
Tip 1: OnPush Is the Default in Angular 22
@Component({
selector: 'app-optimized',
template: `...`
})
export class OptimizedComponent {
@Input() data: any;
constructor(private cd:
ChangeDetectorRef) {}
updateData(): void {
this.data = { ...newData };
this.cd.markForCheck();
}
}
Tip 2: Choose a Stable track Expression
`
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
`
Tip 3: Unsubscribe Patterns Comparison
private sub: Subscription;
ngOnInit() { this.sub = observable.subscribe(); }
ngOnDestroy() { this.sub?.unsubscribe(); }
private destroy$ = new
Subject();
ngOnInit() {
observable.pipe(takeUntil(this.destroy$)).subscribe();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
data$ = this.service.getData();
13. Testing Lifecycle Hooks
Testing Example:
describe('LifecycleComponent', () => {
let component: LifecycleComponent;
let fixture: ComponentFixture<LifecycleComponent>;
const service = { getData: vi.fn() };
beforeEach(() => {
service.getData.mockReturnValue(of(mockData));
TestBed.configureTestingModule({
imports: [LifecycleComponent],
providers: [{ provide: DataService, useValue: service }]
});
fixture = TestBed.createComponent(LifecycleComponent);
component = fixture.componentInstance;
});
it('should load data in ngOnInit', () => {
expect(service.getData).not.toHaveBeenCalled();
fixture.detectChanges();
expect(component.data).toEqual(mockData);
});
it('should react to input changes', () => {
fixture.componentRef.setInput('inputValue', 'new value');
fixture.detectChanges();
expect(component.lastChange).toBe('new value');
});
it('should clean up on destroy', () => {
fixture.detectChanges();
fixture.destroy();
expect(component.subscription.closed).toBe(true);
});
it('should access ViewChild after view init', () => {
fixture.detectChanges();
expect(component.viewChild).toBeDefined();
});
});
14. Moving from Lifecycle Hooks to Modern Patterns
Lifecycle hooks are still supported, so this move is optional.
graph TD
A[Traditional Lifecycle] --> B{Migration Path}
B --> C[ngOnInit]
C --> D[constructor + inject]
B --> E[ngOnDestroy]
E --> F[DestroyRef.onDestroy]
B --> G[ngOnChanges]
G --> H[Signal inputs]
B --> I[ViewChild/ContentChild]
I --> J[viewChild signal query]
D --> K[Modern Angular]
F --> K
H --> K
J --> K
Migration Examples:
@Component({...})
export class OldComponent implements
OnInit, OnDestroy {
@Input() data: string;
private subscription: Subscription;
constructor(private service: DataService) {}
ngOnInit() {
this.subscription = this.service.getData().subscribe();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
@Component({...})
export class ModernComponent {
data = input<string>();
private service = inject(DataService);
private destroyRef = inject(DestroyRef);
constructor() {
effect(() => {
console.log('Data changed:', this.data());
});
this.service.getData()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe();
this.destroyRef.onDestroy(() => {
console.log('Cleanup logic here');
});
}
}
15. Lifecycle Hooks Cheatsheet
๐ฏ ngOnChanges
- When: Input properties change
- Use: React to @Input changes
- Receives: SimpleChanges object
- Called: Multiple times
๐ ngOnInit
- When: After first ngOnChanges
- Use: Component initialization
- Receives: Nothing
- Called: Once only
๐ ngDoCheck
- When: Every change detection
- Use: Custom change detection
- Receives: Nothing
- Called: Very frequently
๐ฆ ngAfterContentInit
- When: After content projection
- Use: Access @ContentChild
- Receives: Nothing
- Called: Once only
โ
ngAfterContentChecked
- When: After content checked
- Use: Respond to content changes
- Receives: Nothing
- Called: After every check
๐จ ngAfterViewInit
- When: After view initialized
- Use: Access @ViewChild, DOM
- Receives: Nothing
- Called: Once only
๐๏ธ ngAfterViewChecked
- When: After view checked
- Use: Respond to view changes
- Receives: Nothing
- Called: After every check
๐งน ngOnDestroy
- When: Before destruction
- Use: Cleanup resources
- Receives: Nothing
- Called: Once only