Lifecycle hooks describe when Angular creates inputs, initializes a view, checks it, renders it, and destroys it. Use a hook only when work truly depends on that lifecycle stage, and always pair subscriptions, observers, and timers with cleanup.
Angular creates a component instance, assigns its inputs, checks projected content and the component view, renders DOM updates, and eventually destroys the instance. Lifecycle APIs let code run at a precise boundary in that sequence.
A constructor is ordinary TypeScript class setup, not an Angular lifecycle hook. Keep dependency injection and field initialization there. Use a hook only when the work depends on inputs, queries, rendering, repeated checks, or destruction.
| Hook | When it runs | Common use |
|---|---|---|
| constructor | When JavaScript creates the class instance | Inject dependencies and initialize fields |
| ngOnChanges() | After Angular assigns one or more inputs; first run precedes ngOnInit | Compare current and previous input values |
| ngOnInit() | Once after initial inputs, before the component view initializes | Initialize state that depends on inputs |
| ngDoCheck() | Whenever Angular checks this component | Rare manual detection of changes Angular cannot observe |
| ngAfterContentInit() | Once after projected content initializes | Read a decorator-based ContentChild query |
| ngAfterContentChecked() | After every content check | Rare observation of checked projected content |
| ngAfterViewInit() | Once after the component view and child views initialize | Read a decorator-based ViewChild query |
| ngAfterViewChecked() | After every view check | Rare observation after child views are checked |
| afterNextRender() | Once, after the next complete application render | One-time DOM measurement or third-party widget setup |
| afterEveryRender() | After every complete application render | Repeated DOM synchronization when unavoidable |
| ngOnDestroy() | Once immediately before instance destruction | Stop timers, observers, listeners, and long-lived streams |
Each API below has a different timing guarantee. Implement only the boundaries the component needs; an ordinary component often needs none of them.
The constructor runs when JavaScript creates the class instance. Angular has established an injection context, but input values and the component view are not initialized yet.
Use it for dependency injection, field setup, and registering APIs that require an injection context. Do not read inputs, view queries, or projected content here.
ngOnChanges runs after Angular assigns one or more inputs. Its first call happens before ngOnInit, and later calls contain only the inputs assigned during that update.
Use SimpleChanges to compare previousValue and currentValue or detect firstChange. It observes Angular input assignments, not deep mutation inside an unchanged object reference.
ngOnInit runs once after all initial inputs have values and before the component view is initialized.
Use it for one-time state setup that depends on inputs. Do not read view children or rendered DOM here; use a view hook or render callback for that timing.
ngDoCheck runs whenever Angular checks this component, including the initial check after ngOnInit.
Use it only for a narrowly measured change that normal inputs, signals, or immutable updates cannot represent. It is a hot path, so avoid network calls, subscriptions, deep comparisons, and state churn.
ngAfterContentInit runs once after content projected through ng-content has initialized.
Use it to read initialized decorator-based ContentChild or ContentChildren queries. Reading is safe; changing already-checked state here can cause ExpressionChangedAfterItHasBeenCheckedError.
ngAfterContentChecked runs after every check of the component's projected content, not merely once during creation.
Because it runs frequently, prefer reactive content queries and computed state. Use this hook only when repeated post-check observation is unavoidable, and do not mutate checked state inside it.
ngAfterViewInit runs once after the component's own template and child views initialize.
Use it to read initialized decorator-based ViewChild or ViewChildren queries. For DOM measurement after the browser-facing render is committed, afterNextRender is the more precise boundary.
ngAfterViewChecked runs after every check of the component view and its child views.
It is a frequent hook and usually signals that a reactive design would be clearer. Avoid changing checked state or performing expensive DOM work here.
afterNextRender registers a callback that runs once after the next complete application render to the DOM. It is a standalone function, not a class method.
Register it in an injection context for one-time browser DOM work or measurement. It does not run during server-side rendering or build-time prerendering.
afterEveryRender registers a callback after every complete application render. Like afterNextRender, it is application-wide rather than tied to one component instance.
Use it only for repeated synchronization with a non-reactive DOM library. Keep the callback small and use write and read phases when layout properties are involved.
ngOnDestroy runs once immediately before Angular destroys the component or directive instance.
Use it to stop timers, observers, listeners, sockets, and other work that can outlive the view. DestroyRef and takeUntilDestroyed can keep setup and teardown closer together when that is clearer.
ngOnInit runs exactly once after Angular has supplied the initial input values. It is appropriate for deriving initial state or starting work that depends on those values. It runs before the component's own template has been initialized, so view queries and rendered DOM are not ready.
Do not move every constructor statement into ngOnInit. Field defaults and dependency injection belong with class construction; lifecycle-dependent work belongs in the hook.
import { Component, OnInit, input, signal } from '@angular/core';
@Component({
selector: 'app-stock-level',
template: `<p>{{ productCode() }}: {{ status() }}</p>`
})
export class StockLevelComponent implements OnInit {
productCode = input.required<string>();
status = signal('Not checked');
ngOnInit(): void {
this.status.set(`Ready to check ${this.productCode()}`);
}
}
For productCode="KB-42": KB-42: Ready to check KB-42
The required input has its initial value when ngOnInit runs. The example derives initial display state from that value without assuming the view DOM already exists.
ngOnChanges runs whenever Angular assigns changed inputs. Its SimpleChanges object contains one entry per changed property, including previousValue, currentValue, and firstChange. During creation, the first ngOnChanges runs before ngOnInit.
The SimpleChanges key is the TypeScript property name even when an input has a public alias. Mutating a property inside the same object does not represent a new input assignment; prefer immutable replacement when a child must observe that change.
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);
}
}
}
// Signal inputs can also be read in ngOnChanges.
// Use the TypeScript property name as the SimpleChanges key.
Guard each entry because only the inputs changed in the current update appear in SimpleChanges. firstChange distinguishes the initial assignment from later updates.
ngOnDestroy runs once before Angular removes the component, such as after route navigation or when an @if branch stops rendering it. Clean up work that can continue independently of the view: intervals, DOM listeners, observers, WebSockets, and long-lived subscriptions.
Modern Angular can keep setup and teardown together with DestroyRef. RxJS code can use takeUntilDestroyed so the subscription completes with the component instead of maintaining a Subscription field manually.
import { Component, DestroyRef, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({
selector: 'app-timer',
standalone: true,
template: `<p>Elapsed: {{ seconds() }}s</p>`
})
export class TimerComponent {
private destroyRef = inject(DestroyRef);
seconds = signal(0);
constructor() {
interval(1000)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.seconds.update(value => value + 1));
}
}
takeUntilDestroyed completes the interval subscription when Angular destroys this component. A finite HttpClient request normally completes by itself, while an interval does not.
The component view is the markup declared in its own template. ngAfterViewInit runs once after that view and its child views initialize, which makes decorator-based view queries available. Signal query functions such as viewChild stay reactive as the template changes.
Reading a query is safe here; changing checked state in this hook can trigger ExpressionChangedAfterItHasBeenCheckedError. For DOM work that must happen after the application has committed a render, prefer afterNextRender.
import { AfterViewInit, Component, ElementRef, viewChild } from '@angular/core';
@Component({
selector: 'app-focus',
template: `<input #nameInput type="text" placeholder="Auto-focused" />`
})
export class FocusComponent implements AfterViewInit {
nameInput = viewChild.required<ElementRef<HTMLInputElement>>('nameInput');
ngAfterViewInit(): void {
this.nameInput().nativeElement.focus();
}
}
The required query is valid because the input is always present. If an @if could remove it, use optional viewChild and handle undefined.
Content means markup projected into a component through ng-content. ngAfterContentInit runs once after projected children initialize; ngAfterContentChecked runs after each later content check. These hooks concern content supplied by the parent, not elements declared in the component's own template.
Use ngAfterContentInit for a decorator-based ContentChild read. Avoid ngAfterContentChecked unless no reactive alternative exists because it runs frequently, and do not mutate already-checked state from either hook.
import { AfterContentInit, Component, contentChild, Directive } from '@angular/core';
@Directive({ selector: '[cardTitle]' })
export class CardTitleDirective {}
@Component({
selector: 'app-card',
template: `<section><ng-content /></section>`
})
export class CardComponent implements AfterContentInit {
title = contentChild(CardTitleDirective);
ngAfterContentInit(): void {
console.log('Projected title present:', Boolean(this.title()));
}
}
contentChild returns a signal that Angular keeps current. The hook marks the point at which projected content has completed its first initialization.
ngDoCheck runs whenever Angular checks the component, including the first pass after ngOnInit. It exists for manual change tracking that normal input assignments, signals, and immutable state cannot express.
Because the hook is hot-path code, never perform network requests, create subscriptions, or run expensive deep comparisons in it. Prefer signals, computed values, input transforms, or an iterable/key-value differ when those tools model the change.
afterNextRender runs once after the next complete application render. afterEveryRender runs after every complete render. They are standalone functions registered in an injection context and are the correct boundary for DOM work that must observe committed layout.
Render callbacks do not run during server-side rendering or build-time prerendering. Optional write and read phases help avoid layout thrashing by grouping DOM writes before measurements.
import { Component, ElementRef, afterNextRender, inject } from '@angular/core';
@Component({
selector: 'app-chart-shell',
template: `<div class="chart-host">Chart</div>`
})
export class ChartShellComponent {
private host = inject(ElementRef<HTMLElement>);
constructor() {
afterNextRender({
write: () => {
this.host.nativeElement.style.padding = '16px';
},
read: () => {
console.log(this.host.nativeElement.getBoundingClientRect().height);
}
});
}
}
The write phase changes layout first; the read phase measures after all scheduled writes. The callback is skipped during SSR, so browser-only DOM APIs remain inside the browser render boundary.
Choose the narrowest lifecycle boundary. Use ngOnChanges for assigned inputs, ngOnInit for one-time input-dependent setup, query signals for reactive child references, afterNextRender for committed DOM layout, and DestroyRef or ngOnDestroy for teardown.
| Need | Preferred API | Avoid |
|---|---|---|
| Derive a value from reactive state | computed() | Recomputing it in ngDoCheck |
| React to a signal input | computed() or effect() for a true side effect | Duplicating derived state in ngOnChanges |
| Read a child that may appear or disappear | viewChild() or contentChild() signal | Assuming a required query always exists |
| Measure rendered layout | afterNextRender() | Reading layout during checked hooks |
| End a long-lived RxJS stream | takeUntilDestroyed() | A forgotten manual subscription array |
ngOnChanges reacts when Angular assigns a new value to the input binding. Mutating a nested property preserves the same object reference, so Angular may have no input assignment to report through SimpleChanges.
Clean up streams that can outlive the component: intervals, WebSockets, shared subjects, DOM events, and similar sources. A normal HttpClient request completes, and the async pipe cleans up its own subscription. For long-lived RxJS work, takeUntilDestroyed is usually tidier than maintaining a collection of Subscription objects.
The required query assumes that the matching element or child component exists when the view query is evaluated. In the page example, #nameInput is always present, so reading it in ngAfterViewInit is safe. If the input is placed inside an @if block that is false during initialization, the required query has no result and fails.
Explore 500+ free tutorials across 20+ languages and frameworks.