An Angular chart component must measure its container after rendering: why use afterNextRender rather than ngAfterViewInit, and which phases?
answer
- checked is not painted
- all components, not this one
- earlyRead, write, mixedReadWrite, read
- skipped during server rendering
- resizes are not renders
basics
~20 sngAfterViewInit runs mid change detection, per component, and also on the server. afterNextRender runs once after Angular has rendered every component to the DOM, only in the browser; measure in earlyRead and draw in write.
solid answer
~50 s`ngAfterViewInit` fires while Angular is still walking the tree: this component's view is initialized, but other components may not be rendered yet, it also runs during server rendering where there is no layout, and changing bound state there throws `NG0100` in development. `afterNextRender`, registered in the constructor, runs once after the whole application has rendered to the DOM and is skipped on the server. It accepts phases, `earlyRead`, `write`, `mixedReadWrite` (the default for a bare callback) and `read`, and Angular runs each phase across **all** registered callbacks before the next, so writes are batched ahead of reads and layout thrashing is avoided. For the chart, read the container size in `earlyRead`, whose return value is passed to `write`, and draw there. For later resizes, attach a `ResizeObserver` and disconnect it through `DestroyRef`, rather than re-measuring in `afterEveryRender` after every render.
code
ts · 32 linesimport { Component, DestroyRef, ElementRef, afterNextRender, inject, viewChild } from '@angular/core';
@Component({
selector: 'app-sales-chart',
template: `<div #plot class="plot"></div>`,
})
export class SalesChart {
private readonly plot = viewChild.required<ElementRef<HTMLDivElement>>('plot');
constructor() {
const destroyRef = inject(DestroyRef);
afterNextRender({
// Read the size the first draw depends on.
earlyRead: () => this.plot().nativeElement.getBoundingClientRect(),
// Receives earlyRead's return value.
write: (rect) => {
this.draw(rect.width, rect.height);
const observer = new ResizeObserver(([entry]) =>
this.draw(entry.contentRect.width, entry.contentRect.height),
);
observer.observe(this.plot().nativeElement);
destroyRef.onDestroy(() => observer.disconnect());
},
});
}
private draw(width: number, height: number): void {
// Hand the size to the charting library; skip if it has not changed,
// because ResizeObserver also reports the initial size.
}
}go deeper
Recall that afterNextRender runs once after Angular has rendered everything to the DOM, is registered in the constructor, and is where browser-only DOM work goes.
Explain the four phases, that each runs across all callbacks before the next, and how a phase's return value reaches the next phase.
Diagnose the chart bug: measurement mid change detection, NG0100 from storing sizes, server execution, and layout thrashing. Choose earlyRead plus write for the first draw and ResizeObserver for later changes.
Weigh where DOM integration code should live across a component library: shared render-callback helpers, cost of afterEveryRender, and how phase discipline keeps many widgets from thrashing layout together.
## The problem A chart component wraps a charting library that needs real pixel dimensions: it must read its container's width and height from the DOM, then draw. Reading layout at the wrong moment gives zeros, stale sizes, or forced synchronous layouts that make the page janky. Angular offers two candidate moments, and they are not equivalent. ## Why ngAfterViewInit is the wrong moment `ngAfterViewInit` is a **lifecycle hook**, called during **change detection**, Angular's top-down walk that brings bindings up to date. It means "this component's view and its children are initialized", nothing more: - **Other components may not be rendered yet.** The walk continues after your hook returns, so a sibling that changes layout later can invalidate your measurement. - **It runs on the server too.** During server-side rendering there is no layout engine; `getBoundingClientRect()` is meaningless and browser-only chart code may fail. - **State changes are dangerous.** Storing the measured size in a field that the template binds throws `NG0100` in development mode, because those bindings were already checked. - **Reads are not coordinated.** If ten charts each read then write in their own hook, the browser may recompute layout between every pair. ## What afterNextRender guarantees `afterNextRender(callbackOrPhases, options?)` registers a **render callback**. Key properties, all from Angular's contract: - It runs **once**, after Angular has finished rendering **all** components to the DOM. Its sibling `afterEveryRender` runs after every render. - It must be called in an **injection context**, typically the constructor, or be given `{ injector }`; otherwise it throws `NG0203`. - It does **not** run during server-side rendering or build-time prerendering; on the server it returns a no-op reference. - It is tied to the component's `DestroyRef`: if the component is destroyed first, the callback is dropped, unless you pass `manualCleanup: true`. - It returns an `AfterRenderRef` whose `destroy()` cancels it. ## The four phases | Phase | Allowed work | Guidance | |---|---|---| | `earlyRead` | read layout that a later write depends on | avoid unless strictly needed | | `write` | write layout-affecting properties | never read here | | `mixedReadWrite` | read and write together | default for a bare callback; avoid | | `read` | read layout | never write here | Two mechanics make phases worthwhile: 1. **Cross-component batching.** Angular runs `earlyRead` for every registered callback, then `write` for every callback, then `mixedReadWrite`, then `read`. All writes across the page land before any `read`, so the browser lays out once instead of once per component. 2. **Pipelining.** A phase function may return a value; the next phase callback of the same registration receives it as its argument. The first phase that runs receives nothing. ## afterNextRender versus afterEveryRender Both take the same arguments and phases; the difference is how often they fire. - **`afterNextRender`** runs after the next render and is then removed. It fits one-time integration: instantiating a third-party widget, focusing an element, taking a first measurement. - **`afterEveryRender`** stays registered and runs after every render of the application until the component is destroyed or you call `destroy()` on the returned ref. It fits work that must track Angular's own DOM updates, such as syncing a non-Angular library with content Angular just changed. Because an application renders often, anything placed in `afterEveryRender` should be cheap and should prefer the `write` and `read` phases over `mixedReadWrite`. If a callback only needs to react to one piece of state, reacting to that state (for example with a signal) is usually cheaper than running on every render. ## The chart, done right 1. In the constructor, call `afterNextRender` with an `earlyRead` that returns the container's `DOMRect`; the chart genuinely needs that size before it can draw, which is exactly what `earlyRead` is for. 2. In `write`, create or draw the chart with the width and height it received. 3. For resizes, create a `ResizeObserver` on the container and register `observer.disconnect()` with `DestroyRef`. Why not `afterEveryRender` for step 3? It runs after **every** application render, triggered by any state change anywhere, so a layout read there costs work on every render even when the container did not move. A container resize, meanwhile, is not itself a render. `ResizeObserver` reports exactly the size changes you care about. ## Pitfalls - Registering `afterNextRender` in `ngOnInit` or a click handler without `{ injector }`: `NG0203`. - Writing in `read` or reading in `write`: this defeats the batching. - Assuming render callbacks run inside the change-detection pass: they run after it, outside the Angular zone in zone-based apps. - Using the old name: `afterRender` was renamed `afterEveryRender` in v20.
- Why not re-measure the container in afterEveryRender?`afterEveryRender` runs after every application render, whatever caused it, so a layout read there costs work on every render even when the container never changed. A container resize is not itself a render, so the callback would also miss resizes that happen without state changes. `ResizeObserver` reports exactly the size changes the chart needs.
- What happens to a pending afterNextRender callback if the component is destroyed before the next render?By default the registration is tied to the `DestroyRef` of the injection context, so destroying the component unregisters it and the callback never runs. Passing `manualCleanup: true` opts out of that; you then keep the returned `AfterRenderRef` and call its `destroy()` yourself.
- What does the value a phase callback returns do?It is passed as the argument to the next phase callback of the same registration, so `earlyRead` can hand a measurement to `write`, and `write` can tell `read` whether anything changed. The first phase that runs receives no argument.
saying these in an interview costs you the question
- ngAfterViewInit means the browser has laid out and painted the whole page.
- afterNextRender fires right after this one component renders, like a per-component hook.
- Storing a measured size in a bound field inside ngAfterViewInit is safe.
- afterEveryRender is the right tool for reacting to container resizes.
- afterNextRender can be called from a click handler without passing an injector.
- mixedReadWrite is the recommended phase for DOM measurement.