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.
| Hook | When it runs | Typical use |
|---|---|---|
afterRender | After every render (every change detection that touches the DOM) | Continuously syncing the DOM with a non-Angular library |
afterNextRender | After the next render only, then never again | One-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.
| Phase | Purpose | DOM access |
|---|---|---|
earlyRead | Read layout before any writes | Read |
write | Mutate the DOM | Write |
mixedReadWrite | Last resort when read/write can’t be split | Read + Write |
read | Read layout after all writes | Read |
Warning: Avoid
mixedReadWrite. It defeats the batching optimization and reintroduces layout thrashing. Split intoearlyRead/write/readwhenever 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
afterNextRenderfor one-time DOM setup; reserveafterRenderfor 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
isPlatformBrowserchecks 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 inngOnDestroy. - Never trigger signal or state changes inside these callbacks expecting an immediate re-render — they run outside change detection.