Exploring Angular’s afterRender and afterNextRender Hooks

Netanel Basal
Netanel Basal
Published in
4 min readApr 26, 2024

--

Occasionally, there arises a need to employ browser-specific APIs for manual DOM manipulation. However, navigating this task becomes challenging when considering lifecycle events, which are also triggered during server-side rendering and pre-rendering processes. Angular addresses this challenge by introducing the afterRender and afterNextRender features. These utilities ensure that code execution remains exclusive to the browser context, providing a reliable solution for such scenarios.

It’s worth noting that the execution of render callbacks is not tied to any specific component instance but rather serves as an application-wide hook.

The afterNextRender Hook

The afterNextRender hook, albeit not the most descriptive name, takes a callback function that runs once after the subsequent change detection cycle. Typically, afterNextRender is ideal for performing one-time initializations, such as integrating third-party libraries or utilizing browser-specific APIs:

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

@Component({
selector: 'app-chart',
template: `<div #chart></div>`,
})
export class MyChartCmp {
chart = viewChild.required<ElementRef>('chart');

chart: ChartRef;

constructor() {
afterNextRender(() => {
this.chart = new ChartLib(this.chart().nativeElement);
});
}
}

Looking at the source code, when Angular invokes tick() to initiate a new change detection cycle from top to bottom in the component tree, it calls detectChangesInAttachedViews, which in turn invokes afterRenderEffectManager.execute() to execute any callbacks registered using these hooks.

The afterNextRender callback executes once per tick and then destroys itself. This implies that while we're not restricted to calling it only once, each invocation ensures it runs just once after the subsequent tick. Here's a simplified example:

@Component()
export class MyCmp {

constructor() {
afterNextRender(() => {
console.log('run once')
});

setTimeout(() => {
afterNextRender(() => {
console.log('run once')
})
}, 5000)
}
}

Both hooks must be invoked within the injection context and can accept an injector if there’s a need to run them outside of this context:

@Component()
export class MyCmp {
private injector = inject(Injector);

onClick() {
afterNextRender(() => {
// Do something
}, { injector: this.injector });
}
}

You can find numerous examples of this hook’s usage in the Angular component repository.

The afterRender Hook

Unlike the afterNextRender hook, which executes only once, the afterRender hook triggers after every change detection cycle. It serves as a valuable tool for DOM synchronization tasks, providing an escape route when the browser lacks a more suitable API for such operations. Utilize afterRender to conduct additional reads or writes to the DOM each time Angular completes its mutations:

@Component({
selector: 'my-cmp',
template: `<span #content></span>`,
})
export class MyComponent {
content = viewChild.required<ElementRef>('content');

constructor() {
afterRender(() => {
console.log(this.content().nativeElement.scrollHeight);
});
}
}

You can find numerous examples of this hook’s usage in the Angular component repository. It’s essential to understand that it executes after every tick, regardless of whether your component necessitates a change detection check (such as with OnPush). Hence, it's advisable to wield it with caution.

Managing Hook Phases

When employing afterRender or afterNextRender, you have the option to specify a phase, offering precise control over the sequencing of DOM operations. This capability allows you to arrange write operations before read operations, thereby minimizing layout thrashing.

@Component({...})
export class ExampleComponent {
private elementWidth = 0;
private elementRef = inject(ElementRef);

constructor() {
const nativeElement = this.elementRef.nativeElement;

// Write phase: Adjusting width.
afterNextRender(() => {
nativeElement.style.width = computeWidth();
}, { phase: AfterRenderPhase.Write });

// Read phase: Retrieving final width after adjustments.
afterNextRender(() => {
this.elementWidth = nativeElement.getBoundingClientRect().width;
}, { phase: AfterRenderPhase.Read });
}
}

The phases operate in a predetermined sequence:

  1. EarlyRead: This phase is ideal for reading any layout-affecting DOM properties and styles necessary for subsequent calculations. If possible, minimize usage of this phase, favoring the Write and Read phases.
  2. MixedReadWrite: This phase serves as the default option. It’s suitable for operations requiring both read and write access to layout-affecting properties and styles. However, it’s advisable to use the explicit Write and Read phases whenever possible.
  3. Write: Opt for this phase when setting layout-affecting DOM properties and styles.
  4. Read: Utilize this phase for reading layout-affecting DOM properties.

Manual Unregistration

By default, Angular automatically unregisters callbacks upon component destruction, leveraging the DestroyRef mechanism. However, if you need to unregister manually, each hook provides a destroy function that you can invoke:

@Component({
selector: 'my-cmp',
template: `<span #content></span>`,
})
export class MyComponent {
content = viewChild.required<ElementRef>('content');

constructor() {
const ref = afterRender(() => {
console.log(this.content().nativeElement.scrollHeight);
});

// Call this whenever you need
ref.destroy()
}
}

Zone Methods Replacements

Angular is transitioning towards a zoneless environment, prompting the replacement of certain methods. Among these, the NgZone.onMicrotaskEmpty and NgZone.onStable observables are commonly utilized to await Angular’s completion of change detection before executing a task. However, these can now be substituted with afterNextRender if waiting for a single change detection, or afterRender if a condition may span multiple change detection rounds.

Follow me on Medium or Twitter to read more about Angular and JS!

--

--

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.