Angular components have a well-defined lifecycle: they are created, rendered, updated when inputs change, and eventually destroyed. Lifecycle hooks let you run code at specific points in this lifecycle. ngOnInit is the most commonly used — it runs after Angular has set all input properties, making it the right place to load data from the API. ngOnDestroy is equally important — subscriptions and timers not cleaned up here cause memory leaks and ghost requests. Angular 16+ provides takeUntilDestroyed() as the modern, cleaner alternative to manual subscription management.
Lifecycle Hooks in Practice
import {
Component, OnInit, OnChanges, OnDestroy, AfterViewInit,
Input, SimpleChanges, inject, signal,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { PostsService } from './posts.service';
@Component({ selector: 'app-post-detail', standalone: true, template: '...' })
export class PostDetailComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit {
@Input({ required: true }) slug!: string;
post = signal<PostDto | null>(null);
loading = signal(true);
private postsService = inject(PostsService);
private destroyRef = inject(DestroyRef); // for takeUntilDestroyed
// ── ngOnInit ─── after first ngOnChanges, inputs are set ─────────────────
ngOnInit(): void {
// ✅ Correct: load data here (inputs are available)
this.loadPost(this.slug);
// ❌ Wrong: do NOT put data loading in constructor
// (inputs not yet set in constructor)
}
// ── ngOnChanges ─── runs BEFORE ngOnInit and on every input change ────────
ngOnChanges(changes: SimpleChanges): void {
// React to @Input changes after the component is initialised
if (changes['slug'] && !changes['slug'].firstChange) {
// Reload when slug input changes (navigating between posts)
this.loadPost(changes['slug'].currentValue);
}
}
// ── ngAfterViewInit ─── after template is fully rendered ──────────────────
ngAfterViewInit(): void {
// Access @ViewChild references here (not in ngOnInit — not yet rendered)
// Focus an input, initialise a third-party chart library, etc.
}
// ── ngOnDestroy ─── cleanup before component is removed ──────────────────
ngOnDestroy(): void {
// Manual approach: cancel subscriptions, clear timers
// Modern approach: takeUntilDestroyed() (shown below)
}
private loadPost(slug: string): void {
this.loading.set(true);
// ── Modern: takeUntilDestroyed — auto-unsubscribes on destroy ─────────
this.postsService.getBySlug(slug)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: post => { this.post.set(post); this.loading.set(false); },
error: () => { this.loading.set(false); },
});
}
}
constructor → ngOnChanges (first, with input values) → ngOnInit → ngAfterContentInit → ngAfterViewInit → (update cycles: ngOnChanges → ngDoCheck) → ngOnDestroy. The key practical rules: put data loading in ngOnInit (not constructor), react to input changes in ngOnChanges (check !firstChange to skip the initial call), access @ViewChild references in ngAfterViewInit (not before), and clean up in ngOnDestroy.takeUntilDestroyed() from @angular/core/rxjs-interop (Angular 16+) as the standard pattern for managing RxJS subscriptions in components. It automatically unsubscribes when the component is destroyed, without needing a Subject-based destroy pattern (private destroy$ = new Subject()) or manual ngOnDestroy implementation. Inject DestroyRef in the constructor and pass it to takeUntilDestroyed(this.destroyRef) for the most explicit and flexible usage.HttpClient complete after the HTTP response — no cleanup needed. Subscriptions to Router.events, ActivatedRoute.params, FormControl.valueChanges, and any interval() or timer() do NOT complete automatically — they must be unsubscribed in ngOnDestroy or via takeUntilDestroyed(). Unmanaged subscriptions on destroyed components cause memory leaks and “ExpressionChangedAfterItHasBeenCheckedError” errors.Lifecycle Hook Checklist
| Hook | When | Use For |
|---|---|---|
| constructor | Component instantiation | DI injection only; no inputs yet |
| ngOnChanges | Before ngOnInit + on each input change | React to @Input changes |
| ngOnInit | Once after first ngOnChanges | Initial data loading, setup |
| ngAfterViewInit | After template fully rendered | Access @ViewChild, third-party libs |
| ngOnDestroy | Before component removed from DOM | Unsubscribe, clear timers |
Common Mistakes
Mistake 1 — Loading data in constructor instead of ngOnInit (inputs not set yet)
❌ Wrong — constructor(private svc: PostsService) { svc.getBySlug(this.slug); } — this.slug is undefined.
✅ Correct — load data in ngOnInit() where all @Input properties are guaranteed to be set.
Mistake 2 — Not unsubscribing from long-lived observables (memory leak)
❌ Wrong — subscribing to router.events without cleanup; subscription persists after component destroyed.
✅ Correct — add .pipe(takeUntilDestroyed(this.destroyRef)) to all non-completing subscriptions.