Angular Lifecycle
Introduction
Every Angular component has a lifecycle managed by the framework itself. Angular creates components, renders them, creates and renders their children, checks for data changes, and destroys them before removing them from the DOM. During this lifecycle, Angular provides several "hooks" that give developers the opportunity to act on components at specific moments.
Understanding Angular's component lifecycle is crucial for efficient application development. It helps you know when to fetch data, when to perform initialization logic, and how to properly clean up resources to prevent memory leaks.
Component Lifecycle Hooks Overview
Angular provides eight lifecycle hooks that correspond to different phases of a component's life:
- ngOnChanges - Called when input properties change
- ngOnInit - Called once after the first ngOnChanges
- ngDoCheck - Called during every change detection run
- ngAfterContentInit - Called after content (ng-content) has been projected into the view
- ngAfterContentChecked - Called after every check of projected content
- ngAfterViewInit - Called after component's view and child views are initialized
- ngAfterViewChecked - Called after every check of component's view and child views
- ngOnDestroy - Called just before Angular destroys the component
Let's dive deeper into each of these hooks.
Implementing Lifecycle Hooks
To use a lifecycle hook, you need to implement the corresponding interface from @angular/core
.
import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-lifecycle-demo',
template: '<div>{{ data }}</div>'
})
export class LifecycleDemoComponent implements OnInit, OnDestroy {
data: string = 'Initial data';
ngOnInit() {
console.log('Component initialized');
this.data = 'Updated data during initialization';
}
ngOnDestroy() {
console.log('Component is being destroyed');
// Clean up resources
}
}
Detailed Look at Each Lifecycle Hook
1. ngOnChanges
This hook is called when any data-bound property of a component changes. It receives a SimpleChanges
object containing the current and previous property values.
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-changes-demo',
template: '<p>Current value: {{ value }}</p>'
})
export class ChangesDemoComponent implements OnChanges {
@Input() value: string = '';
ngOnChanges(changes: SimpleChanges) {
console.log('Previous value:', changes.value?.previousValue);
console.log('Current value:', changes.value?.currentValue);
if (changes.value && !changes.value.firstChange) {
console.log('Value has changed after the first time');
}
}
}
2. ngOnInit
This hook is called once, after the first ngOnChanges
and the component is initialized. It's perfect for initialization logic like fetching data from APIs.
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
<div *ngIf="!user">Loading...</div>
`
})
export class UserProfileComponent implements OnInit {
user: any = null;
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.getUser(1).subscribe(
user => this.user = user,
error => console.error('Error fetching user:', error)
);
}
}
3. ngDoCheck
This hook is called during every change detection run, right after ngOnChanges
and ngOnInit
. It's useful for detecting changes that Angular doesn't catch on its own.
import { Component, DoCheck, Input } from '@angular/core';
@Component({
selector: 'app-do-check',
template: '<div>{{ list.length }} items in the list</div>'
})
export class DoCheckComponent implements DoCheck {
@Input() list: any[] = [];
previousLength = 0;
ngDoCheck() {
if (this.list.length !== this.previousLength) {
console.log('List size changed from', this.previousLength, 'to', this.list.length);
this.previousLength = this.list.length;
}
}
}
4. & 5. ngAfterContentInit & ngAfterContentChecked
These hooks are called after Angular projects external content into the component using <ng-content>
.
import { Component, AfterContentInit, AfterContentChecked, ContentChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-content-demo',
template: `
<div>
<h3>Content Demo Component</h3>
<ng-content></ng-content>
</div>
`
})
export class ContentDemoComponent implements AfterContentInit, AfterContentChecked {
@ContentChild('projectedContent') projectedContent: ElementRef;
ngAfterContentInit() {
console.log('Content initialized');
if (this.projectedContent) {
console.log('Projected content:', this.projectedContent.nativeElement.textContent);
}
}
ngAfterContentChecked() {
console.log('Content checked');
}
}
Usage:
<app-content-demo>
<p #projectedContent>This content is projected into the component</p>
</app-content-demo>
6. & 7. ngAfterViewInit & ngAfterViewChecked
These hooks are called after the component's view and child views have been initialized/checked.
import { Component, AfterViewInit, AfterViewChecked, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-view-demo',
template: `
<div>
<h3>View Demo Component</h3>
<p #viewParagraph>This paragraph is in the component's view</p>
</div>
`
})
export class ViewDemoComponent implements AfterViewInit, AfterViewChecked {
@ViewChild('viewParagraph') paragraph: ElementRef;
ngAfterViewInit() {
console.log('View initialized');
console.log('Paragraph text:', this.paragraph.nativeElement.textContent);
}
ngAfterViewChecked() {
console.log('View checked');
}
}
8. ngOnDestroy
This hook is called just before Angular destroys the component, and is ideal for cleanup tasks like unsubscribing from observables.
import { Component, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-destroy-demo',
template: '<div>Counter: {{ counter }}</div>'
})
export class DestroyDemoComponent implements OnDestroy {
counter = 0;
private subscription: Subscription;
constructor() {
this.subscription = interval(1000).subscribe(() => {
this.counter++;
console.log('Counter:', this.counter);
});
}
ngOnDestroy() {
console.log('Component is being destroyed, cleaning up subscriptions');
this.subscription.unsubscribe();
}
}
Lifecycle Hooks Execution Order
When all hooks are implemented, they're called in this specific order:
- ngOnChanges
- ngOnInit
- ngDoCheck
- ngAfterContentInit
- ngAfterContentChecked
- ngAfterViewInit
- ngAfterViewChecked
- ngOnDestroy
Let's see a comprehensive example showing the complete lifecycle:
import {
Component,
OnChanges,
OnInit,
DoCheck,
AfterContentInit,
AfterContentChecked,
AfterViewInit,
AfterViewChecked,
OnDestroy,
Input,
SimpleChanges
} from '@angular/core';
@Component({
selector: 'app-lifecycle-complete',
template: '<p>{{ name }}</p>'
})
export class LifecycleCompleteComponent implements
OnChanges, OnInit, DoCheck,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
@Input() name: string = '';
constructor() {
console.log('Constructor called');
}
ngOnChanges(changes: SimpleChanges) {
console.log('ngOnChanges called', changes);
}
ngOnInit() {
console.log('ngOnInit called');
}
ngDoCheck() {
console.log('ngDoCheck called');
}
ngAfterContentInit() {
console.log('ngAfterContentInit called');
}
ngAfterContentChecked() {
console.log('ngAfterContentChecked called');
}
ngAfterViewInit() {
console.log('ngAfterViewInit called');
}
ngAfterViewChecked() {
console.log('ngAfterViewChecked called');
}
ngOnDestroy() {
console.log('ngOnDestroy called');
}
}
Real-World Examples
Example 1: Loading Data with ngOnInit
import { Component, OnInit } from '@angular/core';
import { ProductService } from './product.service';
import { Product } from './product.model';
@Component({
selector: 'app-product-list',
template: `
<div *ngIf="loading">Loading products...</div>
<div *ngIf="error">{{ error }}</div>
<div *ngIf="!loading && !error">
<h2>Product List</h2>
<ul>
<li *ngFor="let product of products">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
</div>
`
})
export class ProductListComponent implements OnInit {
products: Product[] = [];
loading = true;
error = '';
constructor(private productService: ProductService) {}
ngOnInit() {
this.productService.getProducts().subscribe(
(data) => {
this.products = data;
this.loading = false;
},
(err) => {
this.error = 'Failed to load products: ' + err.message;
this.loading = false;
}
);
}
}
Example 2: Cleaning Up Resources with ngOnDestroy
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription, interval } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-timer',
template: `
<h3>Timer: {{ seconds }} seconds</h3>
<button (click)="pause()">{{ isPaused ? 'Resume' : 'Pause' }}</button>
`
})
export class TimerComponent implements OnInit, OnDestroy {
seconds = 0;
isPaused = false;
private timerSubscription: Subscription;
private destroy$ = new Subject<void>();
ngOnInit() {
this.startTimer();
}
startTimer() {
this.timerSubscription = interval(1000)
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
if (!this.isPaused) {
this.seconds++;
}
});
}
pause() {
this.isPaused = !this.isPaused;
}
ngOnDestroy() {
// This prevents memory leaks by unsubscribing when component is destroyed
this.destroy$.next();
this.destroy$.complete();
}
}
Example 3: Tracking Changes with ngOnChanges
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-price-tracker',
template: `
<div class="price" [ngClass]="priceChangeClass">
Current Price: ${{ currentPrice }}
<span *ngIf="difference !== 0">
({{ difference > 0 ? '+' : '' }}{{ difference | number:'1.2-2' }})
</span>
</div>
`,
styles: [`
.increase { color: green; }
.decrease { color: red; }
.unchanged { color: black; }
`]
})
export class PriceTrackerComponent implements OnChanges {
@Input() currentPrice: number = 0;
previousPrice: number = 0;
difference: number = 0;
priceChangeClass: string = 'unchanged';
ngOnChanges(changes: SimpleChanges) {
if (changes.currentPrice) {
// Skip initial change
if (!changes.currentPrice.firstChange) {
this.previousPrice = changes.currentPrice.previousValue;
this.difference = this.currentPrice - this.previousPrice;
if (this.difference > 0) {
this.priceChangeClass = 'increase';
} else if (this.difference < 0) {
this.priceChangeClass = 'decrease';
} else {
this.priceChangeClass = 'unchanged';
}
} else {
this.previousPrice = this.currentPrice;
}
}
}
}
Best Practices for Angular Lifecycle Hooks
-
Use ngOnInit for initialization logic instead of the constructor. Constructors should be used only for injecting dependencies.
-
Clean up in ngOnDestroy to avoid memory leaks. Always unsubscribe from Observables, detach event listeners, and clear timers.
-
Avoid heavy operations in ngDoCheck, ngAfterContentChecked, and ngAfterViewChecked as they run frequently during change detection.
-
Use ngOnChanges to react to input changes instead of setting up your own watchers.
-
Avoid updating the view in ngAfterViewInit and ngAfterContentInit as it will trigger a new change detection cycle and may result in the "Expression has changed after it was checked" error.
Common Pitfalls
1. Expression has changed after it was checked error
import { Component, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-error-demo',
template: '<p>{{ value }}</p>'
})
export class ErrorDemoComponent implements AfterViewInit {
value = 'Initial value';
ngAfterViewInit() {
// This will cause an error in development mode
this.value = 'Updated value';
// Fix: Use setTimeout to update the value in the next change detection cycle
setTimeout(() => {
this.value = 'Updated value';
});
}
}
2. Not unsubscribing from Observables
// Bad practice
import { Component } from '@angular/core';
import { interval } from 'rxjs';
@Component({
selector: 'app-memory-leak',
template: '<p>{{ count }}</p>'
})
export class MemoryLeakComponent {
count = 0;
constructor() {
// This will cause a memory leak when the component is destroyed
interval(1000).subscribe(() => this.count++);
}
}
// Good practice
import { Component, OnDestroy, OnInit } from '@angular/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-no-leak',
template: '<p>{{ count }}</p>'
})
export class NoLeakComponent implements OnInit, OnDestroy {
count = 0;
private subscription: Subscription;
ngOnInit() {
this.subscription = interval(1000).subscribe(() => this.count++);
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
Summary
Angular's component lifecycle hooks provide a powerful way to tap into key moments during a component's existence. By understanding when each hook is called and their intended purpose, you can write more efficient and bug-free Angular applications.
- ngOnChanges: React to input property changes
- ngOnInit: Initialize your component
- ngDoCheck: Implement custom change detection
- ngAfterContentInit/Checked: Work with projected content
- ngAfterViewInit/Checked: Work with the component's view
- ngOnDestroy: Clean up before component destruction
Remember to follow the best practices when implementing these hooks, especially cleaning up resources in ngOnDestroy to avoid memory leaks.
Additional Resources
- Angular Documentation on Lifecycle Hooks
- Understanding Change Detection in Angular
- RxJS Best Practices in Angular
Exercises
- Create a timer component that starts when initialized and cleans up properly when destroyed.
- Build a component that displays a different message based on the value of an input property and logs all lifecycle events.
- Create a parent-child component structure where the parent passes data to the child, and implement all relevant lifecycle hooks to track how data flows through the components.
- Implement a component that tracks user activity (mouse movements) and properly cleans up event listeners when destroyed.
- Create a debounced search component using RxJS and lifecycle hooks to ensure proper resource cleanup.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)