Angular's reactivity model has evolved dramatically since the introduction of Signals. Alongside this fine-grained change detection came a new generation of render-aware hooks: afterNextRender(), afterEveryRender(), afterRenderEffect().
These hooks bridge the gap between reactivity and the actual DOM rendering phase, replacing many workarounds that previously relied on setTimeout() or heavy lifecycle juggling.
In this article, we'll explore how each hook works — not through dry theory, but via four hands-on exercises that reveal when and why to use them.
By the end, we'll compare afterNextRender() with the old ngAfterViewInit() to see exactly why the new model is more predictable and fine-grained.
Task 1 — The First Render with afterNextRender()
Let's start simple:
import { Component, afterNextRender } from '@angular/core';
@Component({
selector: 'app-timer',
standalone: true,
template: `<div #el>Timer works!</div>`,
})
export class TimerComponent {
constructor() {
afterNextRender(() => {
console.log('DOM ready');
});
}
}
Question: When does console.log('DOM ready') fire?
Answer: After the component's first DOM render — not immediately, and not during change detection.
afterNextRender() waits for the commit phase (the point when Angular finishes rendering to the DOM). It's perfect for DOM measurements, focus handling, or library initialization that must happen after the HTML is painted.
Task 2 — Continuous Updates with afterEveryRender()
Now let's see what happens when we re-render a component multiple times:
import { Component, signal, afterEveryRender } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div>{{ count() }}</div>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
constructor() {
afterEveryRender(() => {
console.log('Rendered with count =', this.count());
});
}
increment() {
this.count.update(v => v + 1);
}
}
What happens:
- Logs once after the first render,
- Then again after every re-render triggered by a signal update.
afterEveryRender() doesn't react to all signals in the app — only when this component is actually re-rendered.
It's ideal for syncing DOM state or performing measurements when the view changes.
Task 3 — Reactive DOM Effects with afterRenderEffect()
This one brings everything together — reactivity and rendering:
code class="p-2 bg-gray-100 dark:bg-gray-900 overflow-x-auto language-typescript hljs" data-highlighted="yes">import { Component, signal, afterRenderEffect } from '@angular/core'; @Component({ selector: 'app-progress', standalone: true, template: `<progress [value]="progress()" max="100"></progress>`, }) export class ProgressComponent { progress = signal(0); constructor() { afterRenderEffect(() => { console.log('Render effect with progress', this.progress()); if (this.progress() < 100) { this.progress.update(v => v + 25); } }); } }
Result: Logs five times — 0 → 25 → 50 → 75 → 100.
Each time, Angular commits the DOM and then re-runs the effect because the signal changed.
afterRenderEffect() automatically tracks signal dependencies and re-runs whenever they change and a new render occurs.
It's like a render-aware effect — the cleanest way to tie DOM updates to reactive data.
Task 4 — Comparing ngAfterViewInit() vs afterNextRender()
Let's look at a side-by-side example — a classic certification trap.
import { Component, ElementRef, ViewChild, afterNextRender } from '@angular/core';
@Component({
selector: 'app-render-vs-viewinit',
standalone: true,
template: `
<div #box class="demo-box">
<h2>Demo</h2>
<p>Content with font and padding.</p>
</div>
`,
styles: [`
.demo-box {
padding: 24px;
border: 1px solid #ccc;
/* simulate delayed style application */
animation: applyHeight 0s 1 forwards;
}
@keyframes applyHeight {
to { margin-top: 20px; }
}
`]
})
export class RenderVsViewInitComponent {
@ViewChild('box', { static: true }) box!: ElementRef<HTMLDivElement>;
constructor() {
afterNextRender(() => {
const h = this.box.nativeElement.getBoundingClientRect().height;
console.log('[afterNextRender] height =', h);
});
}
ngAfterViewInit() {
const h = this.box.nativeElement.getBoundingClientRect().height;
console.log('[ngAfterViewInit] height =', h);
}
}
Result:
- The first log (
ngAfterViewInit) appears earlier — during the same change detection cycle. - The second (
afterNextRender) runs later, after Angular has committed the DOM and processed layout. - The height measured in
afterNextRender()is often larger or more accurate (reflecting applied CSS/layout changes).
Summary — Old vs. New Hooks
ngAfterViewInit()
- Fires: after the view is created, before DOM commit
- Best for: getting references (
ViewChild) - Works in SSR: Yes
afterNextRender()
- Fires: after the DOM is fully rendered and styles applied
- Best for: DOM measurements, focus, third-party libs
- Works in SSR: No
afterEveryRender()
- Fires: after every re-render of the component
- Best for: logging, syncing, UI instrumentation
- Works in SSR: No
afterRenderEffect()
- Fires: after each render caused by signal changes
- Best for: reactive side effects tied to DOM
- Works in SSR: No
Takeaways
-
afterNextRender()is asynchronous relative to change detection — it "waits" until the DOM is ready. -
afterEveryRender()tracks actual renders, not all reactive updates. -
afterRenderEffect()automatically re-runs based on signal dependencies. - These hooks do not run during SSR, which helps prevent hydration errors.
- Don't mutate state in
afterEveryRender()— it can lead to render loops. UseafterRenderEffect()instead.
Conclusion
Techniques once considered standard — like ngAfterViewInit() and setTimeout() — may now be seen as legacy workarounds. The modern Angular render scheduler and its trio of hooks bring true fine-grained reactivity: predictable, DOM-aware, and cleanly separated from change detection.
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We don't receive any funding, we do this to support the community. ❤️
If you want to show some love, please take a moment to follow me on LinkedIn, TikTok, Instagram. You can also subscribe to our weekly newsletter.
And before you go, don't forget to clap and follow the writer️!
Author: Maciej Osytek
Top comments (0)