Lifecycle Hooks Overview
Every Angular component and directive moves through a predictable sequence of events from the moment Angular creates it to the moment it is torn down. Lifecycle hooks are the named methods you can implement to run code at each of these moments — initializing data, reacting to input changes, working with the rendered DOM, and cleaning up resources. Knowing the order in which Angular calls them is the difference between code that runs reliably and bugs that surface only under change detection.
How a component’s life unfolds
When Angular instantiates a component it first runs the constructor, then begins a series of phases. Some hooks fire exactly once (creation and destruction), while others fire on every change detection cycle (the “check” hooks). Each hook is declared by an interface from @angular/core whose name is the hook name without the ng prefix — for example, the ngOnInit method comes from the OnInit interface.
Implementing the interface is optional at runtime but strongly recommended: it gives you type checking and signals intent. The hook itself is just a method Angular looks for by name.
import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-profile',
standalone: true,
template: `<p>{{ name }}</p>`,
})
export class ProfileComponent implements OnInit, OnDestroy {
name = 'Loading...';
ngOnInit(): void {
this.name = 'Ada Lovelace';
}
ngOnDestroy(): void {
console.log('Profile destroyed — clean up here');
}
}
The hooks and their order
Angular invokes the hooks in a fixed sequence. The table below lists them in the order they run, along with how often each fires.
| Hook | Interface | When it runs |
|---|---|---|
ngOnChanges | OnChanges | Before ngOnInit and whenever a data-bound @Input changes |
ngOnInit | OnInit | Once, after the first ngOnChanges |
ngDoCheck | DoCheck | On every change detection run |
ngAfterContentInit | AfterContentInit | Once, after projected content is initialized |
ngAfterContentChecked | AfterContentChecked | After every check of projected content |
ngAfterViewInit | AfterViewInit | Once, after the component’s view and child views are initialized |
ngAfterViewChecked | AfterViewChecked | After every check of the view and child views |
ngOnDestroy | OnDestroy | Once, just before Angular removes the component |
The key distinction is content versus view. Content refers to elements projected in with <ng-content>; view refers to the component’s own template and its child components. Content hooks always fire before the corresponding view hooks.
Tip:
ngOnInitruns after the firstngOnChanges, so@Input()values are already set whenngOnInitruns. Put input-dependent initialization inngOnInit, not the constructor — inputs are undefined inside the constructor.
Watching the sequence in action
A small component that logs each hook makes the order concrete. The name input is set by a parent, so ngOnChanges fires first.
import {
Component, Input, OnChanges, OnInit, DoCheck,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy,
} from '@angular/core';
@Component({
selector: 'app-trace',
standalone: true,
template: `<span>{{ name }}</span>`,
})
export class TraceComponent
implements OnChanges, OnInit, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
@Input() name = '';
ngOnChanges() { console.log('1 ngOnChanges'); }
ngOnInit() { console.log('2 ngOnInit'); }
ngDoCheck() { console.log('3 ngDoCheck'); }
ngAfterContentInit() { console.log('4 ngAfterContentInit'); }
ngAfterContentChecked() { console.log('5 ngAfterContentChecked'); }
ngAfterViewInit() { console.log('6 ngAfterViewInit'); }
ngAfterViewChecked() { console.log('7 ngAfterViewChecked'); }
ngOnDestroy() { console.log('8 ngOnDestroy'); }
}
Output:
1 ngOnChanges
2 ngOnInit
3 ngDoCheck
4 ngAfterContentInit
5 ngAfterContentChecked
6 ngAfterViewInit
7 ngAfterViewChecked
On every subsequent change detection cycle only the recurring hooks run again — ngDoCheck, ngAfterContentChecked, and ngAfterViewChecked — and ngOnChanges fires only when an input actually changes. ngOnDestroy logs 8 when the component is removed.
Signals and the modern alternatives
Modern Angular increasingly lets you avoid some hooks entirely. Signal inputs (input()) combined with computed() or effect() often replace ngOnChanges, because an effect re-runs automatically when any signal it reads changes.
import { Component, input, effect } from '@angular/core';
@Component({
selector: 'app-greeter',
standalone: true,
template: `<p>Hi, {{ name() }}</p>`,
})
export class GreeterComponent {
name = input.required<string>();
constructor() {
effect(() => console.log('name changed to', this.name()));
}
}
For cleanup, inject(DestroyRef) with takeUntilDestroyed() is a more composable way to tear down subscriptions than wiring everything through ngOnDestroy. For post-render DOM work, the newer afterNextRender and afterRender callbacks are preferred over ngAfterViewInit in zoneless and SSR-aware code.
Best Practices
- Initialize input-dependent state in
ngOnInit, never the constructor — inputs are not yet set when the constructor runs. - Keep
ngDoCheck,ngAfterContentChecked, andngAfterViewCheckedextremely cheap; they run on every change detection pass. - Always clean up subscriptions, timers, and listeners in
ngOnDestroy(or withtakeUntilDestroyed) to avoid memory leaks. - Avoid mutating view state inside
ngAfterViewInit/ngAfterViewCheckedto preventExpressionChangedAfterItHasBeenCheckedError. - Prefer signal inputs,
computed(), andeffect()overngOnChangesfor reactive derivations in new code. - Implement the matching interface for each hook so the compiler verifies your method signatures.