When I first started building Angular applications, one of the biggest hurdles I faced wasn't writing HTML templates or creating services—it was understanding when things actually happen inside a component.
Have you ever tried fetching data from an API inside a component, only to get an undefined error on an input property? Or tried to manipulate a DOM element before Angular finished rendering it? If so, you have bumped headfirst into the Angular Component Lifecycle.
Understanding lifecycle events is like learning the rhythm of Angular. Once you know which hook runs when, you stop fighting the framework and start building smooth, bug-free applications.
What Is a Component Lifecycle?
Think of an Angular component like a living organism. It gets born (created), it grows and updates when new information comes in (change detection), and eventually, it dies (gets removed from the screen).
[Born] [Lives & Updates] [Destroyed]
Creation -------------> Data Changes / Rendering ------------> Cleanup
(constructor, (ngOnChanges, ngOnInit, (ngOnDestroy)
ngOnInit) ngAfterViewInit, etc.)Angular gives us special built-in methods called Lifecycle Hooks. These hooks allow us to step in at specific moments during a component's life and run our own code.
The Execution Order at a Glance
Before diving into each hook, here is the exact chronological order Angular follows during a component's life cycle (starting with JavaScript instantiation):
| Order | Method / Hook | Frequency | Primary Purpose |
|---|---|---|---|
| 1 | constructor() |
Once | Class instantiation & dependency injection. |
| 2 | ngOnChanges() |
Whenever @Input() changes |
Respond to updated input bindings. |
| 3 | ngOnInit() |
Once | Initialize component logic after inputs are ready. |
| 4 | ngDoCheck() |
Every change detection run | Custom change detection for complex state. |
| 5 | ngAfterContentInit() |
Once | After projected content (<ng-content>) is loaded. |
| 6 | ngAfterContentChecked() |
Every change detection run | After projected content has been checked. |
| 7 | ngAfterViewInit() |
Once | After component view & child views are fully loaded. |
| 8 | ngAfterViewChecked() |
Every change detection run | After component view & child views are checked. |
| 9 | ngOnDestroy() |
Once | Cleanup logic right before the component dies. |
Deep Dive into Every Lifecycle Hook
Let me walk you through how each lifecycle hook works in code, why it exists, and when you should use it.
1. The Constructor (constructor)
While not technically an Angular lifecycle hook (it's a standard JavaScript class constructor), it is step 1 where everything begins.
- When it runs: First, when JavaScript creates the component instance.
- Best for: Injecting services via Dependency Injection (DI).
- Avoid: Fetching API data or reading
@Input()values here. Input bindings are not available inside the constructor yet.
import { Component, Input } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-card',
template: `<h2>{{ userTitle }}</h2>`
})
export class UserCardComponent {
@Input() userId!: string;
userTitle = '';
// Good: Inject services here
constructor(private userService: UserService) {
// Bad: this.userId is undefined here!
// console.log(this.userId);
}
}
2. ngOnChanges
ngOnChanges is the first official Angular lifecycle hook to run if your component receives @Input() properties.
- When it runs: Before
ngOnInit(), and every single time an@Input()property changes. - Parameter: It provides a
SimpleChangesobject containing the previous value, current value, and whether it's the first change. - Best for: Reacting instantly whenever a parent component updates a value passed down to this component.
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `<p>User ID: {{ userId }}</p>`
})
export class UserCardComponent implements OnChanges {
@Input() userId!: string;
ngOnChanges(changes: SimpleChanges): void {
if (changes['userId']) {
const prev = changes['userId'].previousValue;
const current = changes['userId'].currentValue;
console.log(`userId changed from ${prev} to ${current}`);
}
}
}
3. ngOnInit
ngOnInit is the most widely used lifecycle hook in Angular development.
- When it runs: Exactly once, right after the first
ngOnChangesrun. At this point, all@Input()properties are fully initialized and ready to use. - Best for: Fetching initial data from backend APIs, initializing complex component properties, and setting up component state.
import { Component, OnInit, Input } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-profile',
template: `<div *ngIf="userData">{{ userData.name }}</div>`
})
export class UserProfileComponent implements OnInit {
@Input() userId!: string;
userData: any;
constructor(private userService: UserService) {}
ngOnInit(): void {
// Perfect place to fetch data!
this.userService.getUser(this.userId).subscribe(data => {
this.userData = data;
});
}
}
4. ngDoCheck
ngDoCheck gives you access to Angular's internal change detection engine.
- When it runs: On every single change detection run—whenever a button is clicked, a timer fires, or an HTTP request completes.
- Best for: Custom change detection when Angular's automatic change detection misses something (e.g., deep mutations inside complex objects or arrays).
- Warning: Code placed here executes constantly. Keep logic minimal to avoid severe performance issues.
import { Component, DoCheck, Input } from '@angular/core';
@Component({
selector: 'app-item-list',
template: `<p>Total items: {{ items.length }}</p>`
})
export class ItemListComponent implements DoCheck {
@Input() items: string[] = [];
private previousLength = 0;
ngDoCheck(): void {
// Detect array mutations that change length without changing reference
if (this.items.length !== this.previousLength) {
console.log('Array length changed!');
this.previousLength = this.items.length;
}
}
}
5 & 6. ngAfterContentInit and ngAfterContentChecked
These hooks deal with Content Projection (using <ng-content> to insert external HTML or components inside your component).
ngAfterContentInit: Fires once after Angular projects external content into the component. Use it when you need to access elements marked with@ContentChildor@ContentChildren.ngAfterContentChecked: Fires after every change detection cycle that checks projected content.
import { Component, AfterContentInit, ContentChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card-header">
<ng-content select="header"></ng-content>
</div>
`
})
export class CardComponent implements AfterContentInit {
@ContentChild('headerText') headerEl!: ElementRef;
ngAfterContentInit(): void {
// Access projected header elements safely
console.log('Projected header content:', this.headerEl.nativeElement.innerText);
}
}
7 & 8. ngAfterViewInit and ngAfterViewChecked
These hooks fire after Angular has finished rendering the component's HTML template and all of its child components.
ngAfterViewInit: Fires once after the template view and child views are fully initialized. This is the first moment you can safely access DOM elements using@ViewChildor@ViewChildren.ngAfterViewChecked: Fires after every change detection check of the view.
import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-custom-input',
template: `<input #searchBox type="text" placeholder="Search..." />`
})
export class CustomInputComponent implements AfterViewInit {
@ViewChild('searchBox') searchInput!: ElementRef;
ngAfterViewInit(): void {
// Focus the input element as soon as the view is ready
this.searchInput.nativeElement.focus();
}
}
Developer Pro-Tip: Do not update component state variables insidengAfterViewInitwithout wrapping them insetTimeout()or triggering a manual change detection, or you will trigger Angular's notoriousExpressionChangedAfterItHasBeenCheckedError.
9. ngOnDestroy (and Modern DestroyRef)
ngOnDestroy is your cleanup worker.
- When it runs: Right before Angular destroys the component and removes its DOM elements.
- Best for: Unsubscribing from RxJS Observables, clearing
setInterval/setTimeouttimers, and detaching event listeners to avoid memory leaks.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-timer',
template: `<p>Timer running...</p>`
})
export class TimerComponent implements OnInit, OnDestroy {
private timerSub!: Subscription;
ngOnInit(): void {
this.timerSub = interval(1000).subscribe(val => console.log(val));
}
ngOnDestroy(): void {
// Always clean up subscriptions to prevent memory leaks!
this.timerSub.unsubscribe();
console.log('Component destroyed, subscription cleaned up.');
}
}
Modern Alternative: DestroyRef and takeUntilDestroyed
In modern Angular, you can avoid writing boilerplate ngOnDestroy methods by using DestroyRef or the RxJS operator takeUntilDestroyed:
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({
selector: 'app-timer',
template: `<p>Modern Clean Timer</p>`
})
export class ModernTimerComponent {
constructor() {
// Automatically unsubscribes when the component is destroyed!
interval(1000)
.pipe(takeUntilDestroyed())
.subscribe(val => console.log(val));
}
}
Modern SSR-Safe Hooks: afterNextRender and afterRender
With modern Angular support for Server-Side Rendering (SSR), traditional hooks like ngAfterViewInit sometimes cause issues because they execute on both the server and the browser.
Angular introduced two modern lifecycle functions designed specifically for safe browser DOM operations:
afterNextRender: Runs once in the browser after the next change detection cycle. Ideal for initializing third-party browser-only libraries (e.g., Chart.js, Leaflet maps, D3).afterRender: Runs after every render cycle in the browser. Useful for re-synchronizing custom canvas elements or third-party DOM components.
import { Component, ElementRef, ViewChild, afterNextRender } from '@angular/core';
@Component({
selector: 'app-chart',
template: `<canvas #chartCanvas></canvas>`
})
export class ChartComponent {
@ViewChild('chartCanvas') canvas!: ElementRef;
constructor() {
// Only runs in the browser, perfectly safe for SSR!
afterNextRender(() => {
// Initialize browser-only library here safely
console.log('Canvas element is safe to manipulate:', this.canvas.nativeElement);
});
}
}
3 Common Mistakes Beginners Make
- Putting API calls inside
constructor()instead ofngOnInit():- Problem: Inputs aren't ready yet, and testing becomes harder.
- Fix: Keep constructors lean (for dependency injection only) and move fetch calls to
ngOnInit().
- Forgetting to unsubscribe in
ngOnDestroy():- Problem: Long-running RxJS subscriptions remain active in memory long after the user leaves the page.
- Fix: Use
takeUntilDestroyed()or explicitly unsubscribe inngOnDestroy().
- Accessing
@ViewChildbeforengAfterViewInit():- Problem: Trying to read
@ViewChildinsidengOnInit()returnsundefinedbecause the view hasn't finished rendering yet. - Fix: Always wait until
ngAfterViewInit()(orafterNextRender()) to manipulate view elements.
- Problem: Trying to read
Mastering these lifecycle events gives you predictable control over your Angular apps. Start simple: rely on ngOnInit for initialization, ngOnChanges for input tracking, ngAfterViewInit for DOM access, and ngOnDestroy for cleanup. As you build more complex features, the remaining hooks will fall naturally into place.