Skip to main content

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:

  1. ngOnChanges - Called when input properties change
  2. ngOnInit - Called once after the first ngOnChanges
  3. ngDoCheck - Called during every change detection run
  4. ngAfterContentInit - Called after content (ng-content) has been projected into the view
  5. ngAfterContentChecked - Called after every check of projected content
  6. ngAfterViewInit - Called after component's view and child views are initialized
  7. ngAfterViewChecked - Called after every check of component's view and child views
  8. 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.

typescript
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.

typescript
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.

typescript
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.

typescript
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>.

typescript
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:

html
<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.

typescript
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.

typescript
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:

  1. ngOnChanges
  2. ngOnInit
  3. ngDoCheck
  4. ngAfterContentInit
  5. ngAfterContentChecked
  6. ngAfterViewInit
  7. ngAfterViewChecked
  8. ngOnDestroy

Let's see a comprehensive example showing the complete lifecycle:

typescript
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

typescript
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

typescript
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

typescript
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

  1. Use ngOnInit for initialization logic instead of the constructor. Constructors should be used only for injecting dependencies.

  2. Clean up in ngOnDestroy to avoid memory leaks. Always unsubscribe from Observables, detach event listeners, and clear timers.

  3. Avoid heavy operations in ngDoCheck, ngAfterContentChecked, and ngAfterViewChecked as they run frequently during change detection.

  4. Use ngOnChanges to react to input changes instead of setting up your own watchers.

  5. 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

typescript
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

typescript
// 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

Exercises

  1. Create a timer component that starts when initialized and cleans up properly when destroyed.
  2. Build a component that displays a different message based on the value of an input property and logs all lifecycle events.
  3. 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.
  4. Implement a component that tracks user activity (mouse movements) and properly cleans up event listeners when destroyed.
  5. 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! :)