Angular 19: afterRenderEffect
Angular 19 will introduce a new powerful feature called afterRenderEffect. This new primitive creates effect(s) that run as part of Angular's afterRender sequence.
What is afterRenderEffect?
afterRenderEffect
is a function that registers an effect to be executed after the application has finished rendering. It's particularly useful for scenarios where you need to read from or write to the DOM, as it ensures that these operations happen at the right time in the rendering lifecycle.
The API itself mirrors afterRender
and afterNextRender
with one big difference: values are propagated from phase to phase as signals
instead of as plain values. As a result, later phases may not need to execute if the values returned by earlier phases do not change.
Key Features of afterRenderEffect
Phased Execution: Effects can be registered for specific phases of the render cycle.
Signal Integration: It works seamlessly with Angular's signal reactivity system.
Performance Optimization: By separating DOM reads and writes, it helps prevent layout thrashing.
Browser-Only Execution: These effects only run in browser environments, not on the server (
SSR
)!Reactive Behavior: Effects run only when dirty through signal dependencies.
Understanding the Phases
afterRenderEffect
introduces four distinct phases:
earlyRead
: For reading from the DOM before any writes occur.write
: For writing to the DOM.mixedReadWrite
: For operations that involve both reading and writing to the DOM.read
: For reading from the DOM after all writes have completed.
These phases run in the order listed above, allowing for optimized DOM operations.
Basic Usage
Let's start with a simple example (follow code here):
import { Component, afterRenderEffect } from '@angular/core';
@Component({
selector: 'app-example',
template: '<div #myElement>Hello, World!</div>',
standalone: true
})
export class ExampleComponent {
constructor() {
/**
* According to the docs:
* You should prefer specifying an explicit phase for the effect instead,
* or you risk significant performance degradation.
*/
afterRenderEffect(() => {
console.log('This runs after the component has rendered');
});
}
}
In this example, the effect will run after the component has rendered, logging a message to the console. By default, this effect runs in the mixedReadWrite
phase.
Advanced Usage with Phases
Now, let's look at a more complex example utilizing different phases:
@Component({...})
export class App {
myElement = viewChild<ElementRef>('myElement');
interval!: any;
height = signal(this.myElement()?.nativeElement?.offsetHeight ?? 1.1);
constructor() {
afterRenderEffect({
write: (onCleanup) => {
this.myElement()!.nativeElement.style.height = `${
this.height() * 1.1
}px`;
onCleanup(() => {
console.log('Clean up write');
});
return { heightSet: this.height() * 1.1 };
},
read: (writeResult) => {
const { heightSet } = writeResult();
console.log(`New height set: ${heightSet}`);
},
});
this.interval = setInterval(() => {
this.resize();
}, 2000);
}
cleanup(): void {
clearInterval(this.interval);
}
resize(): void {
this.height.set(this.height() * 2);
}
}
In this example we increase the height of a div element each 2 seconds and check how the onclean function is executed when we update the signal value.
Note how each phase can return a value, which is then passed as a signal to the next phase. This allows for coordinated work across phases.
Effect becomes dirty
afterRenderEffect
will run:
in the order it they are registered
only when dirty
on browser platforms only (no SSR)
during the
mixedReadWrite
phase
Let’s focus on the dirty sequence because the others are clear. So, if we do this and we reset the log at the end, the effect won’t run again because it’s not dirty, it has no signals changed:
// Won't trigger effect (no signals, no dirty)
const log = [];
afterRenderEffect({
earlyRead: () => {
log.push('earlyRead');
console.log('earlyRead');
},
write: () => log.push('write'),
mixedReadWrite: () => log.push('mixedReadWrite'),
read: () => log.push('read'),
});
log.length = 0;
It should run when made dirty via signal
:
// Will trigger effect (signals change, dirty)
const log = [];
counter = signal(0);
afterRenderEffect({
earlyRead: () => {
log.push('earlyRead');
console.log('earlyRead', counter());
},
write: () => log.push('write'),
mixedReadWrite: () => log.push('mixedReadWrite'),
read: () => log.push('read'),
});
log.length = 0;
counter.set(1);
Should run cleanup functions before re-running phase effects
@Component({
selector: 'app-root',
standalone: true,
})
export class App implements OnInit {
counter = signal(0);
log: string[] = [];
constructor() {
afterRenderEffect({
earlyRead: (onCleanup) => {
onCleanup(() => {
console.log('cleanup earlyRead');
this.log.push('cleanup earlyRead');
});
this.log.push(`earlyRead: ${this.counter()}`);
console.log(`earlyRead: ${this.counter()}`);
// Calculate isEven:
return this.counter() % 2 === 0;
},
write: (isEven, onCleanup) => {
onCleanup(() => {
console.log('cleanup write');
this.log.push('cleanup write');
});
console.log(`write: ${isEven()}`);
this.log.push(`write: ${isEven()}`);
console.log('---------------------');
},
});
}
ngOnInit(): void {
// Initial run should run both effects with no cleanup
console.log('---------------------');
this.log.length = 0;
// A counter of 1 will clean up and rerun both effects.
setTimeout(() => {
console.log('---------------------');
this.counter.set(1);
this.log.length = 0;
}, 3000);
// A counter of 3 will clean up and rerun the earlyRead phase only.
setTimeout(() => {
console.log('---------------------');
this.counter.set(3);
this.log.length = 0;
}, 7000);
// A counter of 4 will then clean up and rerun both effects.
setTimeout(() => {
console.log('---------------------');
this.counter.set(4);
}, 11000);
}
}
And the results in the console from this are:
---------------------
👉 Initial run should run both effects with no cleanup
earlyRead: 0
write: true
---------------------
---------------------
👉 A counter of 1 will clean up and rerun both effects.
cleanup earlyRead
earlyRead: 1
cleanup write
write: false
---------------------
---------------------
👉 A counter of 3 will clean up and rerun the earlyRead phase only.
cleanup earlyRead
earlyRead: 3
---------------------
---------------------
👉 A counter of 4 will then clean up and rerun both effects.
cleanup earlyRead
earlyRead: 4
cleanup write
write: true
---------------------
As you can see, the second phase only re-run when his signal (write) has changed and the cleanup functinos of each phase only executes when his tracking node (signal
) changes.
Note that the cleanup callbacks are also executed when we destroy the hook:
const myEffect = afterRenderEffect({
earlyRead: (onCleanup) => {
onCleanup(() => {
console.log('cleanup earlyRead');
this.log.push('cleanup earlyRead');
});
return this.counter() % 2 === 0;
},
});
// If we destroy our hook effect, the cleanup
// function inside phases will be executed
myEffect.destroy();
You can find lots of examples in the test file of the PR.
Best Practices
Use the appropriate phase for your operations to optimize performance.
Prefer
read
andwrite
phases overearlyRead
andmixedReadWrite
when possible.Use the cleanup function to remove any listeners or observers when the effect is destroyed.
Be cautious with DOM manipulations, as components may not be fully hydrated when the effect runs.
Leverage the signal passing between phases to coordinate complex operations.
Avoid calling
afterRenderEffect
inside a reactive context (e.g., inside another effect or computed signal).
Conclusion
Angular's afterRenderEffect
is a powerful tool for managing post-render logic in your applications. By understanding its phases and using it appropriately, you can create more efficient and responsive Angular applications. Remember to always consider the performance implications of your DOM operations and use afterRenderEffect
to structure them in the most optimal way.
This feature is still marked as experimental, so be sure to check Angular's official documentation for any updates or changes in future versions. Use afterRenderEffect
to create effects that will read or write from the DOM and thus should run after rendering.
If you want to learn more about afterRender
and afterNextRender
I have a video about it. Remember that afterRenderEffect
it’s just a mirror from afterRender with signal values and doing the same output as the effect signal.
Link to code 💻:
https://stackblitz.com/edit/stackblitz-starters-zatv2v?file=src%2Fmain.ts
Link to PR 🌎:
https://github.com/angular/angular/pull/57549
Thanks for reading so far 🙏
I’d like to have your feedback so please leave a comment, clap or follow. 👏
Spread the Angular love! 💜
If you really liked it, share it among your community, tech bros and whoever you want! 🚀👥
Don't forget to follow me and stay updated: 📱
Thanks for being part of this Angular journey! 👋😁