Initialising data, cleaning up subscriptions and touching the DOM safely all depend on lifecycle timing.
Angular calls lifecycle hooks on components and directives as they are created, updated and destroyed. Implement them as methods (with the matching interface for type safety):
ngOnInit()— once, after the first inputs are set. Classic place to start loading data based on inputs.ngOnChanges(changes)— whenever decorator-based inputs change. With signal inputs you rarely need it: usecomputedoreffect.ngOnDestroy()— before the component is removed; clean up timers and subscriptions.ngAfterViewInit()andngAfterContentInit()— after the view or projected content is first created.
`DestroyRef` is the composable alternative to ngOnDestroy: inject(DestroyRef).onDestroy(() => ...) works in services, directives and helper functions, which is how utilities like takeUntilDestroyed clean up automatically.
For DOM work, use the render hooks:
afterNextRender(fn)runs once after the next render, in the browser only. Use it to initialise charts, measure elements or focus inputs.afterRenderEffect(fn)runs after rendering whenever signals it reads change.afterEveryRender(fn)runs after every render (use sparingly).
Render hooks never run during server-side rendering, so they are the safe place for browser APIs. All of these registration functions must be called in an injection context, typically the constructor.
Example
import { Component, DestroyRef, ElementRef, OnInit, afterNextRender, afterRenderEffect,
inject, input, signal, viewChild } from '@angular/core';
@Component({
selector: 'app-clock-chart',
template: `<p>{{ title() }}: {{ now() }}</p><canvas #c width="200" height="20"></canvas>`,
})
export class ClockChart implements OnInit {
title = input('Clock');
protected now = signal(new Date());
private canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('c');
constructor() {
const id = setInterval(() => this.now.set(new Date()), 1000);
inject(DestroyRef).onDestroy(() => clearInterval(id)); // cleanup
afterNextRender(() => console.log('first paint, browser only'));
afterRenderEffect(() => { // re-runs when now() changes
const ctx = this.canvas().nativeElement.getContext('2d')!;
ctx.clearRect(0, 0, 200, 20);
ctx.fillRect(0, 0, this.now().getSeconds() * 3, 20);
});
}
ngOnInit() { console.log('inputs ready:', this.title()); }
}Remember
ngOnInitafter first inputs;ngOnDestroyorDestroyRef.onDestroyfor cleanup- With signal inputs prefer
computed/effectoverngOnChanges afterNextRender/afterRenderEffectfor browser-only DOM work
Watch out for
- Touching
windowor the DOM in the constructor or ngOnInit of an SSR app; use render hooks.