Lifecycle Hooks in Angular
What are Lifecycle Hooks?
Angular calls lifecycle hook methods at specific moments in a component's life — from creation to destruction. Implementing these interfaces lets you tap into those moments to run your own logic.
Lifecycle Sequence
| Hook | When it runs | Common use |
|---|---|---|
ngOnChanges() | Before ngOnInit, every time an @Input changes | React to input changes |
ngOnInit() | Once, after first ngOnChanges | Fetch data, initialize state |
ngDoCheck() | Every change detection cycle | Custom change detection |
ngAfterContentInit() | Once, after content projection | Access @ContentChild |
ngAfterContentChecked() | After every content check | Respond to projected content changes |
ngAfterViewInit() | Once, after view & child views init | Access @ViewChild, DOM manipulation |
ngAfterViewChecked() | After every view check | Respond to view changes |
ngOnDestroy() | Just before component is destroyed | Unsubscribe, cleanup timers |
ngOnInit — Most Common Hook
import { Component, OnInit, inject, signal } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user',
standalone: true,
template: `
@if (user()) {
<h2>{{ user()!.name }}</h2>
} @else {
<p>Loading...</p>
}
`
})
export class UserComponent implements OnInit {
private userService = inject(UserService);
user = signal<any>(null);
ngOnInit() {
// Runs once after component is initialized
// Inputs are available here (unlike the constructor)
this.userService.getUser(1).subscribe(u => this.user.set(u));
}
}
ngOnChanges — Reacting to Input Changes
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
template: `<p>{{ title }}</p>`
})
export class ChildComponent implements OnChanges {
@Input() title = '';
@Input() count = 0;
ngOnChanges(changes: SimpleChanges) {
if (changes['title']) {
const prev = changes['title'].previousValue;
const curr = changes['title'].currentValue;
console.log(`title changed: ${prev} → ${curr}`);
}
if (changes['count']?.firstChange) {
console.log('count set for the first time:', this.count);
}
}
}
// Note: with signal-based input(), use effect() instead of ngOnChanges
ngOnDestroy — Cleanup
import { Component, OnInit, OnDestroy, signal } from '@angular/core';
import { Subscription, interval } from 'rxjs';
@Component({
selector: 'app-timer',
standalone: true,
template: `<p>Elapsed: {{ seconds() }}s</p>`
})
export class TimerComponent implements OnInit, OnDestroy {
seconds = signal(0);
private sub!: Subscription;
ngOnInit() {
this.sub = interval(1000).subscribe(() => {
this.seconds.update(s => s + 1);
});
}
ngOnDestroy() {
// ALWAYS unsubscribe to prevent memory leaks
this.sub.unsubscribe();
console.log('TimerComponent destroyed, subscription cleaned up');
}
}
ngAfterViewInit — Accessing the DOM
import { Component, AfterViewInit, viewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-focus',
standalone: true,
template: `<input #nameInput type="text" placeholder="Auto-focused" />`
})
export class FocusComponent implements AfterViewInit {
// Signal-based ViewChild (Angular 17+)
nameInput = viewChild.required<ElementRef>('nameInput');
ngAfterViewInit() {
// DOM is fully rendered — safe to access elements
this.nameInput().nativeElement.focus();
}
}
Modern Alternative: effect() with Signals
With signal-based inputs (input()), you can use effect() instead of ngOnChanges for a cleaner reactive approach.
import { Component, input, effect, computed } from '@angular/core';
@Component({
selector: 'app-modern',
standalone: true,
template: `<p>{{ greeting() }}</p>`
})
export class ModernComponent {
name = input.required<string>();
greeting = computed(() => `Hello, ${this.name()}!`);
constructor() {
// Runs whenever name() changes — replaces ngOnChanges
effect(() => {
console.log('Name changed to:', this.name());
});
}
}