Skip to content
Angular ng lifecycle 4 min read

afterRender & afterNextRender

Sometimes you need to read or mutate the actual rendered DOM — measuring an element’s size, integrating a third-party charting library, or scrolling to a position. Doing this inside ngOnInit or ngAfterViewInit is fragile, and on the server it crashes because there is no DOM. Angular’s afterRender and afterNextRender hooks solve this cleanly: they only run in the browser, only after Angular has finished rendering, and give you a safe phase-aware API for touching the DOM.

Why these hooks exist

Lifecycle hooks like ngAfterViewInit fire once per component, but they run during change detection and execute on the server during SSR. If you call document.querySelector or read element.offsetHeight there, you either throw a runtime error on the server or trigger layout thrashing in the browser.

afterRender and afterNextRender were introduced in Angular 16 (stabilized in v17+) specifically for DOM-dependent side effects. They are registered with functions — not class methods — and Angular guarantees they:

  • Run only in the browser (never during server-side rendering).
  • Run after every component on the page has rendered, not just the current one.
  • Run outside change detection, so they will not retrigger another render cycle by accident.

afterRender vs afterNextRender

The two hooks share an API but differ in cadence.

HookWhen it runsTypical use
afterRenderAfter every render (every change detection that touches the DOM)Continuously syncing the DOM with a non-Angular library
afterNextRenderAfter the next render only, then never againOne-time setup: measuring, focusing, initializing a widget

Reach for afterNextRender for setup that happens once, and afterRender only when you genuinely need to react to every render.

Basic usage

Both hooks must be called in an injection context — typically a constructor, a field initializer, or inside inject()-friendly code.

import { Component, ElementRef, afterNextRender, inject } from '@angular/core';

@Component({
  selector: 'app-measured-box',
  standalone: true,
  template: `<div #box class="box">Measure me</div>`,
})
export class MeasuredBoxComponent {
  private host = inject(ElementRef<HTMLElement>);

  constructor() {
    afterNextRender(() => {
      const box = this.host.nativeElement.querySelector('.box') as HTMLElement;
      console.log('Box height:', box.offsetHeight);
    });
  }
}

Output:

Box height: 42

On the server this callback simply never runs, so there is no offsetHeight is not defined crash and no isPlatformBrowser guard needed.

Render phases

Reading and writing the DOM in the same callback causes layout thrashing — the browser is forced to recompute layout repeatedly. To avoid this, both hooks accept a phases object so you can split work into earlyRead, write, and read. Angular batches all callbacks of the same phase together across the whole application.

import { Component, ElementRef, afterRender, inject } from '@angular/core';

@Component({
  selector: 'app-auto-resize',
  standalone: true,
  template: `<textarea #area></textarea>`,
})
export class AutoResizeComponent {
  private host = inject(ElementRef<HTMLElement>);

  constructor() {
    let height = 0;

    afterRender({
      earlyRead: () => {
        const area = this.host.nativeElement.querySelector('textarea') as HTMLTextAreaElement;
        return area.scrollHeight; // read only
      },
      write: (scrollHeight) => {
        height = scrollHeight;
        const area = this.host.nativeElement.querySelector('textarea') as HTMLTextAreaElement;
        area.style.height = `${height}px`; // write only
      },
    });
  }
}

The value returned from earlyRead is passed into write, letting you cleanly separate measurement from mutation.

PhasePurposeDOM access
earlyReadRead layout before any writesRead
writeMutate the DOMWrite
mixedReadWriteLast resort when read/write can’t be splitRead + Write
readRead layout after all writesRead

Warning: Avoid mixedReadWrite. It defeats the batching optimization and reintroduces layout thrashing. Split into earlyRead/write/read whenever possible.

Cleaning up

afterRender and afterNextRender return a handle with a destroy() method, and they auto-clean when their injection context (the component) is destroyed. For manual teardown — for example tearing down a third-party widget — capture the handle.

import { Component, afterNextRender } from '@angular/core';
import Chart from 'chart.js/auto';

@Component({
  selector: 'app-chart',
  standalone: true,
  template: `<canvas #canvas></canvas>`,
})
export class ChartComponent {
  private chart?: Chart;

  constructor() {
    afterNextRender(() => {
      const canvas = document.querySelector('canvas')!;
      this.chart = new Chart(canvas, {
        type: 'bar',
        data: { labels: ['A', 'B'], datasets: [{ data: [3, 7] }] },
      });
    });
  }

  ngOnDestroy() {
    this.chart?.destroy();
  }
}

Because afterNextRender runs only in the browser, the Chart.js library is never instantiated during SSR — which is exactly what you want for a canvas-based widget.

Best Practices

  • Prefer afterNextRender for one-time DOM setup; reserve afterRender for logic that truly must react to every render.
  • Use the phase API (earlyRead, write, read) to separate DOM reads from writes and avoid layout thrashing.
  • Register these hooks in an injection context (constructor or field initializer), not inside other lifecycle methods.
  • Treat them as your SSR-safe escape hatch — no manual isPlatformBrowser checks are required.
  • Keep callbacks fast; they run synchronously after rendering and can delay paint if heavy.
  • Tear down third-party widgets created in these hooks via the returned handle’s destroy() or in ngOnDestroy.
  • Never trigger signal or state changes inside these callbacks expecting an immediate re-render — they run outside change detection.
Last updated June 14, 2026
Was this helpful?