Skip to the solution
HardFrontend8 min read

Why use Renderer2 instead of direct DOM access?

338 views

The problem

Question presented to candidate: "A teammate wrote a directive that calls document.createElement and element.style directly. In review you suggest Renderer2. Justify it: what concrete problems does Renderer2 solve, what does it NOT solve, and when would you avoid both?"

What a strong answer should cover:

  • Renderer2 is Angular's abstraction over DOM operations (createElement, appendChild, addClass, setStyle, setAttribute, setProperty, listen), injected per component.
  • Platform independence: on the server there is no global document (a ReferenceError); Renderer2 and inject(DOCUMENT) work in SSR.
  • Encapsulation: elements created through the component's Renderer2 get the emulated-encapsulation attribute, so component styles apply; document.createElement does not.
  • listen() returns an unlisten function; tie it to DestroyRef.
  • It is not a security layer: the 22.2.3 typings say it provides no extra security, and setProperty(el, 'innerHTML', ...) is not sanitised.
  • Best of all: avoid imperative DOM work entirely with bindings / host metadata, and run browser-only work in afterNextRender.

Clarifying questions expected:

  • "Does the app use SSR or prerendering?"
  • "Is the DOM change expressible as a binding instead?"

Code / implementation expected: Yes β€” a directive that adds a class, a style, an attribute, a created child element and a listener through Renderer2, with cleanup.

  • renderer2
  • dom

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 11 sections

Read the code

AngularA directive that adds a class, style, attribute, child element and listener through Renderer2
import { Component, DestroyRef, Directive, ElementRef, Renderer2, inject, signal } from "@angular/core";

@Directive({ selector: "[autoBadge]" })
export class AutoBadgeDirective {
  private host = inject(ElementRef<HTMLElement>).nativeElement;
  private r = inject(Renderer2);
  hovers = signal(0);
  constructor() {
    this.r.addClass(this.host, "badge-host");
    this.r.setStyle(this.host, "border", "1px solid teal");
    this.r.setAttribute(this.host, "data-ready", "yes");
    const badge = this.r.createElement("sup");
    this.r.appendChild(badge, this.r.createText("new"));
    this.r.appendChild(this.host, badge);
    // listen() returns an unlisten function; tie it to the directive lifetime.
    const stop = this.r.listen(this.host, "mouseenter", () => {
      this.hovers.update((n) => n + 1);
      console.log("mouseenter #" + this.hovers());
    });
    inject(DestroyRef).onDestroy(() => {
      stop();
      console.log("listener removed");
    });
  }
}

@Component({
  selector: "app-root",
  imports: [AutoBadgeDirective],
  template: `@if (on()) { <h4 id="t" autoBadge>Inbox</h4> }
    <button (click)="on.set(false)">remove</button>`,
})
export class AppComponent {
  on = signal(true);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 111 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextHard

How do you implement custom routing strategies by extending RouteReuseStrategy?

Continue