In Angular 19, an experimental lifecycle hook, afterRenderEffect
, allows developers to update the DOM reactively.
Like afterNextRender
and afterRender
, afterRenderEffect
has four phases: earlyRead
, write
, mixedReadWrite
, and read
.
Four phases of AfterRenderEffect
- earlyRead: the phase to read from the DOM before the subsequent write. Avoid this phase if the read operation can defer to the read phase. Never read from the DOM in this phase.
- write: In this phase, developers read the signal value and write to the DOM. Never read from the DOM in this phase.
- mixedReadWrite: In this phase, developers can read and write to the DOM. Avoid this phase if
- read: In this phase, developers can read from the DOM, log the new value, or update the signal. Never write to the DOM in this phase.
In this demo, I will use the afterRenderEffect
to manipulate a DIV element to change its shape's clip-path and color. The afterRenderEffect
registers the effect and runs it after all the components are rendered.
Two solutions will be presented: a simple solution that computes the CSS variables using a computed signal and a complex solution that uses the afterRenderEffect
hook. The complex solution is shown here to demonstrate the usage of the afterRenderEffect hook. Otherwise, the simple should be chosen.
I will show the clean solution with a computed signal and then the solution that updates the style in the afterRenderEffect hook.
Solution 1: Create CSS Variables in a Computed Signal
:host {
--shape-color: red;
--clip-path: circle(100px);
}
div.shape {
clip-path: var(--clip-path);
background: var(--shape-color);
}
The host element declares two CSS variables, --clip-path
and --shape-color
, that update the CSS styles of clip-path
and background
in the shape class.
@Component({
selector: 'app-root',
imports: [FormsModule],
template: `
<div><label>Choose a color:
<select [(ngModel)]="color">
@for (c of barColors(); track c.id) {
<option [value]="c.id">{{ c.color }}</option>
}
</select>
</label></div>
<div><label>Choose a shape:
<select [(ngModel)]="shape">
@for (c of shapes(); track c.id) {
<option [ngValue]="c">{{ c.id }}</option>
}
</select>
</label></div>
<div class="frame">
<div class="shape" [style]="cssVariables()"></div>
</div>
`,
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
barColors = signal([
{ id: 'red', color: 'Red' },
{ id: 'rebeccapurple', color: 'Rebecca Purple' },
]);
shapes = signal([
{ id: 'Circle', clipPath: 'circle(100px)' },
{ id: 'Rectangle', clipPath: 'rect(50px 200px 150px 0px)' },
]);
color = signal('red');
shape = signal(this.shapes()[0]);
sanitizer = inject(DomSanitizer);
clipPath = computed(() => this.shape().clipPath)
cssVariables = computed(() => ({
'--clip-path': this.sanitizer.bypassSecurityTrustStyle(this.clipPath()),
'--shape-color': this.sanitizer.bypassSecurityTrustStyle(this.color())
}));
}
The App
component has two drop-down lists for color and shape selection. When the signals have new updates, the cssVariables
computed signal calculates the CSS variables' new value.
There is a DIV element with a shape class, and the cssVariables
computed variable is assigned to the CSS style. Therefore, the shape and color change when the selected values are changed.
This is the end of the clean solution. Let's redo this using the afterRenderEffect
hook for demonstration purposes.
Solution 2: AfterRenderEffect hook
@Component({
selector: 'app-root,'
imports: [FormsModule, NgTemplateOutlet],
template: `
<div><label>Choose a color:
<select [(ngModel)]="color">
@for (c of barColors(); track c.id) {
<option [value]="c.id">{{ c.color }}</option>
}
</select>
</label></div>
<div><label>Choose a shape:
<select [(ngModel)]="shape">
@for (c of shapes(); track c.id) {
<option [ngValue]="c">{{ c.id }}</option>
}
</select>
</label></div>
<div class="frame">
<div class="shape" #el></div>
</div>
`,
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
barColors = signal([
{ id: 'red,' color: 'Red' },
{ id: 'rebeccapurple,' color: 'Rebecca Purple' },
]);
shapes = signal([
{ id: 'Circle,' clipPath: 'circle(100px)' },
{ id: 'Rectangle', clipPath: 'rect(50px 200px 150px 0px)' },
]);
div = viewChild.required<ElementRef<HTMLDivElement>>('el')
nativeElement = computed(() => this.div().nativeElement);
renderer = inject(Renderer2);
constructor() {
afterRenderEffect({
write: () => {
const clipPath = this.clipPath();
const color = this.color();
const safeStyles = `
--clip-path: ${clipPath};
--shape-color: ${color};
`;
this.renderer.setProperty(this.nativeElement(),
'Style', safeStyles);
return safeStyles;
},
read: (safeStyles) => {
console.log(safeStyles());
}
});
}
}
The App
component calls the viewChild
function to query the DIV ElementRef, and the nativeElement
computed signal returns the HTML element. The Renderer2
is also injected to set the style property in the AfterRenderEffect
hook.
The constructor constructs the AfterRenderEffect
hook. The clipath
and color
signals are read to build the CSS style string in the write
phase. The renderer
applies the CSS variables to the style
property of the DIV element. Finally, the write
phase returns the CSS string, so the read
phase receives it and logs it in the console.
Both solutions achieve the same things. However, the afterRenderEffect
hook is recommended when an Angular application uses a third-party library to read and write to the DOM declaratively and reactively.
Resources:
- AfterRenderEffect: https://angular.dev/api/core/afterRenderEffect
- TechstackNation AfterRenderEffect: https://www.youtube.com/watch?v=6alUfY9ec3Q&t=3s
Top comments (0)