The lifecycle hooks (ngOnInit, ngOnChanges, ngOnDestroy...) still exist and work. But with signals, most of them become unnecessary: deriving from inputs is done by computed(), reactions by effect(), DOM work by afterNextRender(), and cleanup by DestroyRef.
The equivalence table
Old hook → Modern equivalent
───────────────────────────────────────────────────────────────────
constructor → constructor (DI, signals, effect, afterNextRender)
ngOnChanges → computed() / effect() / linkedSignal() on inputs
ngOnInit → computed() / resource() (or ngOnInit, still valid)
ngDoCheck → you almost never need it
ngAfterContentInit → contentChild() / contentChildren() (signals)
ngAfterViewInit → afterNextRender() + viewChild()
ngAfterViewChecked → afterEveryRender() (rarely)
ngOnDestroy → inject(DestroyRef).onDestroy() / takeUntilDestroyed()What a modern component looks like
import { Component, DestroyRef, afterNextRender, computed, effect, inject, input } from '@angular/core';
@Component({ selector: 'app-user-panel', /* ... */ })
export class UserPanel {
readonly userId = input.required<number>();
private readonly api = inject(UserApi); // DI: in fields / constructor
// “ngOnChanges” → computed / effect / resource on inputs
readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);
readonly title = computed(() => this.user.value()?.name ?? 'Loading…');
constructor() {
// “ngAfterViewInit” → afterNextRender (once, after the first render)
afterNextRender(() => this.chart.init(this.canvas().nativeElement));
// “ngOnDestroy” → DestroyRef.onDestroy (works in services, functions, directives too)
inject(DestroyRef).onDestroy(() => this.chart.dispose());
// reaction to an input changing
effect(() => this.analytics.track('user-panel-open', { id: this.userId() }));
}
}- Press “Create the child”.
ngOnChanges with typed SimpleChanges v21
// Still need ngOnChanges? From v21, SimpleChanges is generic:
export class Legacy implements OnChanges {
readonly name = input('');
readonly age = input(0);
ngOnChanges(changes: SimpleChanges<Legacy>) {
if (changes.name) { // typed, without changes['name']
console.log(changes.name.previousValue, '→', changes.name.currentValue);
}
}
}DestroyRef: reusable cleanup
Unlike ngOnDestroy (which exists only on classes), DestroyRef can be injected anywhere — including in functions. That allows reusable “hooks” that clean up by themselves.
// DestroyRef works anywhere there is an injection context — ideal for reusable functions
export function useInterval(ms: number, fn: () => void): void {
const id = setInterval(fn, ms);
inject(DestroyRef).onDestroy(() => clearInterval(id));
}
export class Clock {
readonly now = signal(new Date());
constructor() {
useInterval(1000, () => this.now.set(new Date())); // cleans up by itself
}
}afterNextRender / afterEveryRender
// afterNextRender — once; afterEveryRender — after every render of the application (rarely needed)
afterNextRender({
read: () => this.el.nativeElement.offsetHeight, // reads the layout
// write: (height) => ... // then writes (separate phases = no thrashing)
});
// Runs only in the browser (not during SSR): the right place for window, document, localStorage#1 Did the build pass?
#2 Yes, and all the tests are green.
#3 Then I’ll open the PR.
#4 Ping me when it’s merged.
send() {
this.messages.update((list) => [...list, next]);
const el = this.chat().nativeElement;
el.scrollTop = el.scrollHeight; // the DOM still has the old list
}Cleaning up subscriptions, timers and listeners is covered in detail in Memory leaks: finding and fixing them; code that touches window or document must also run on the server: Server-compatible code.
When YES, when NO
- Existing code that works — there is no need to force a rewrite.
ngOnInitfor initialisation that reads inputs once (it is still valid and readable).- Interfaces required by libraries (e.g. the classic
ControlValueAccessor).
- Recalculating a value when an input changes →
computed(). - Resetting local state when an input changes →
linkedSignal(). - Fetching when an input changes →
httpResource(). - Unsubscribing in
ngOnDestroy→takeUntilDestroyed()/toSignal.
Common pitfalls
- Inputs in the constructor: they are not set. An
input.requiredread there throws an error. window/documentin the constructor with SSR → an error on the server. Move the code intoafterNextRender.ngOnChangesdoes not fire for internal mutations of objects you receive — only for new references.