Every component has a life: Angular creates it, shows it, updates it and finally destroys it. Lifecycle hooks are methods that Angular calls at those moments, so you can run your code at the right time.
What you will learn
- The order of the hooks
- What
ngOnInit,ngOnDestroy,ngOnChangesandngDoCheckare for - The after-view and after-content hooks
- Modern signal-based alternatives
The order
For one component Angular calls, in this order:
constructor(not a hook, but it runs first)ngOnChanges: when an input changes (beforengOnInitand on later changes)ngOnInit: once, after the first inputs are setngDoCheck: on every change detection runngAfterContentInitandngAfterContentChecked: after projected content is readyngAfterViewInitandngAfterViewChecked: after the component's view is readyngOnDestroy: just before the component is removed
See the hooks run
// src/app/lifecycle/lifecycle-logger.ts
import {
AfterContentChecked,
AfterContentInit,
AfterViewChecked,
AfterViewInit,
Component,
DoCheck,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
input,
output,
} from '@angular/core';
@Component({
selector: 'app-lifecycle-logger',
template: `<p>Hello, {{ name() }}! <ng-content /></p>`,
})
export class LifecycleLogger
implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy
{
readonly name = input('friend');
readonly log = output<string>();
constructor() {
this.log.emit('constructor');
}
ngOnChanges(changes: SimpleChanges): void {
// changes['name'] has previousValue, currentValue and firstChange
console.log('ngOnChanges', changes);
}
ngOnInit(): void {
console.log('ngOnInit: name is', this.name());
}
ngDoCheck(): void {
// Runs very often. Keep it cheap, and do not log here in real code.
}
ngAfterContentInit(): void {
console.log('ngAfterContentInit');
}
ngAfterContentChecked(): void {}
ngAfterViewInit(): void {
console.log('ngAfterViewInit');
}
ngAfterViewChecked(): void {}
ngOnDestroy(): void {
console.log('ngOnDestroy');
}
}
Each hook is an interface you implement. Implementing the interface is optional, but TypeScript then warns you about typos such as ngOnInt.
ngOnInit: start-up work
Use ngOnInit for work that needs the inputs, such as loading data for an id. The constructor is for simple setup like injecting services, because inputs are not available yet.
// src/app/lifecycle/user-card.ts
import { Component, OnInit, inject, input, signal } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `<p>{{ status() }}</p>`,
})
export class UserCard implements OnInit {
readonly userId = input.required<number>();
protected readonly status = signal('not loaded');
ngOnInit(): void {
// In real code: this.api.load(this.userId()).subscribe(...)
this.status.set(`Loaded user ${this.userId()}`);
}
}
ngOnDestroy: clean up
Anything you start must be stopped: timers, event listeners on window, Observable subscriptions.
// src/app/lifecycle/ticker.ts
import { Component, OnDestroy, OnInit, signal } from '@angular/core';
@Component({
selector: 'app-ticker',
template: `<p>Ticks: {{ ticks() }}</p>`,
})
export class Ticker implements OnInit, OnDestroy {
protected readonly ticks = signal(0);
private timer?: ReturnType<typeof setInterval>;
ngOnInit(): void {
this.timer = setInterval(() => this.ticks.update((n) => n + 1), 1000);
}
ngOnDestroy(): void {
clearInterval(this.timer); // without this the timer runs forever
}
}
A modern shortcut is inject(DestroyRef).onDestroy(() => ...), which lets you register cleanup right where you start the work. takeUntilDestroyed() does the same for Observables.
ngOnChanges: react to input changes
ngOnChanges receives a SimpleChanges object with the previous and current value of each changed input:
// src/app/lifecycle/price-tag.ts
import { Component, OnChanges, SimpleChanges, input, signal } from '@angular/core';
@Component({
selector: 'app-price-tag',
template: `<p>{{ price() }} <span>{{ trend() }}</span></p>`,
})
export class PriceTag implements OnChanges {
readonly price = input(0);
protected readonly trend = signal('');
ngOnChanges(changes: SimpleChanges): void {
const change = changes['price'];
if (change && !change.firstChange) {
this.trend.set(change.currentValue > change.previousValue ? 'up' : 'down');
}
}
}
With signal inputs, you usually do not need ngOnChanges. Use computed() for derived values, or effect() for side effects, which re-run automatically when the input changes. ngOnChanges still fires for signal inputs (we checked this on Angular 22) and for older @Input() properties, so existing code keeps working.
ngDoCheck
ngDoCheck runs on every change detection pass. It exists for custom change detection, such as noticing a mutated object that Angular's default checks cannot see. Because it runs so often, keep its code very small. Most apps never need it.
After-content and after-view hooks
- `ngAfterContentInit` runs once after content projected with
<ng-content />is ready. Use it withcontentChildandcontentChildren. - `ngAfterViewInit` runs once after the component's own template, and its child components, are rendered. Use it to measure or focus elements found with
viewChild. - The
...Checkedversions run after every check. Avoid heavy work in them.
Changing state that the template uses inside these hooks can trigger the ExpressionChangedAfterItHasBeenChecked error in development, which a later lesson explains.
Modern alternatives
New code often uses functions instead of hooks:
- Run once in the browser after render: Modern way:
afterNextRender(() => ...) - Run after every render: Modern way:
afterEveryRender(() => ...) - Clean up: Modern way:
inject(DestroyRef).onDestroy(...) - React to signal changes: Modern way:
effect()andcomputed()
Hooks are not deprecated, and you will see them everywhere, so know both styles.
Put it together
// src/app/lifecycle/lifecycle-page.ts
import { Component, signal } from '@angular/core';
import { LifecycleLogger } from './lifecycle-logger';
import { PriceTag } from './price-tag';
import { Ticker } from './ticker';
import { UserCard } from './user-card';
@Component({
selector: 'app-lifecycle-page',
imports: [LifecycleLogger, PriceTag, Ticker, UserCard],
template: `
<button type="button" (click)="show.set(!show())">Toggle component</button>
<button type="button" (click)="price.update(p => p + 5)">Raise price</button>
@if (show()) {
<app-lifecycle-logger [name]="'Ada'">projected</app-lifecycle-logger>
<app-ticker />
}
<app-price-tag [price]="price()" />
<app-user-card [userId]="7" />
`,
})
export class LifecyclePage {
protected readonly show = signal(true);
protected readonly price = signal(10);
}
// src/app/app.ts
import { Component } from '@angular/core';
import { LifecyclePage } from './lifecycle/lifecycle-page';
@Component({
imports: [LifecyclePage],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
<h1>Lifecycle</h1>
<app-lifecycle-page />
</main>
Open the console and click "Toggle component" to watch ngOnDestroy and a new ngOnInit.
Try it yourself
- Log
ngOnInitin two child components and see the order when both are on the page. - Add a window
resizelistener inngOnInitand remove it inngOnDestroy. - Replace the ticker's
ngOnDestroywithinject(DestroyRef).onDestroy.
Quick check
- Which hook runs first after the constructor when an input is set?
- Where do you stop a timer?
- Why keep
ngDoChecksmall?
Next, see how components find and control elements in template reference variables and queries. Read the lifecycle guide.