Lifecycle hooks, DestroyRef and render hooks

Angular · Practical · 11 min

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: use computed or effect.
  • ngOnDestroy() — before the component is removed; clean up timers and subscriptions.
  • ngAfterViewInit() and ngAfterContentInit() — 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

  • ngOnInit after first inputs; ngOnDestroy or DestroyRef.onDestroy for cleanup
  • With signal inputs prefer computed/effect over ngOnChanges
  • afterNextRender / afterRenderEffect for browser-only DOM work

Watch out for

  • Touching window or the DOM in the constructor or ngOnInit of an SSR app; use render hooks.

Check yourself

  1. Where should you initialise a third-party chart library that needs the canvas element, in an app that also renders on the server?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/components/lifecycle