Skip to the solution
HardFrontend8 min read

What does ChangeDetectorRef do?

1.1k views

The problem

Question presented to candidate: "You inject ChangeDetectorRef into a component. What can it do, when would you reach for markForCheck() versus detectChanges(), and why do you need it less in a signals-based Angular 22 app?"

What a strong answer should cover:

  • ChangeDetectorRef is the handle to one component's view in the change detection tree; in v22 its public API is exactly four methods: markForCheck, detectChanges, detach, reattach.
  • markForCheck() flags the view and its ancestors and lets the scheduler run later (asynchronous); detectChanges() checks this view and its children right now (synchronous), whether or not anything is flagged.
  • detach() removes the view from the tree so normal runs skip it even when marked; detectChanges() still works on a detached view, which enables manual, throttled rendering.
  • Under the v22 defaults (OnPush, zoneless) a plain field changed in async code needs one of these; a signal needs neither, and AsyncPipe calls markForCheck() internally.
  • checkNoChanges() was removed from the public ChangeDetectorRef API in v22; tests use fixture.detectChanges() instead.

Clarifying questions expected:

  • "Is the state in signals already, or is this legacy code with plain fields and Observables?"
  • "Is the goal to fix a stale view, or to deliberately throttle rendering of a hot component?"

Code / implementation expected: Yes β€” show markForCheck() and detectChanges() updating a stale OnPush view, and detach() blocking normal updates.

  • change-detection
  • cdr

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 Β· 9 sections

Read the code

AngularmarkForCheck vs detectChanges vs detach on an OnPush component with a plain field
import { ChangeDetectorRef, Component, ViewChild, inject, signal } from "@angular/core";

// A separate log component: its signal re-renders only this view,
// so it never refreshes the counter by accident.
@Component({
  selector: "event-log",
  template: `<ol>@for (line of lines(); track $index) { <li>{{ line }}</li> }</ol>`,
})
export class EventLog {
  lines = signal<string[]>([]);
  add(line: string) { this.lines.update(l => [...l, line]); }
}

// OnPush is the v22 default, and count is a PLAIN field, not a signal.
@Component({
  selector: "plain-counter",
  template: `<p id="count">count: {{ count }}</p>`,
})
export class PlainCounter {
  count = 0;
  cdr = inject(ChangeDetectorRef);
  shown = () => document.getElementById("count")!.textContent;
}

@Component({
  selector: "app-root",
  imports: [EventLog, PlainCounter],
  template: `
    <plain-counter #c />
    <button id="silent" (click)="later(c, 'silent')">++ in setTimeout</button>
    <button id="mark" (click)="later(c, 'markForCheck')">++ then markForCheck()</button>
    <button id="detect" (click)="later(c, 'detectChanges')">++ then detectChanges()</button>
    <button id="detach" (click)="toggle(c)">{{ attached ? "detach()" : "reattach()" }}</button>
    <event-log />
  `,
})
export class AppComponent {
  attached = true;
  @ViewChild(EventLog) log!: EventLog;

  later(c: PlainCounter, how: "silent" | "markForCheck" | "detectChanges") {
    // setTimeout: no template event is involved, so nothing marks the view dirty for us.
    setTimeout(() => {
      c.count++;
      if (how === "markForCheck") c.cdr.markForCheck();   // schedules a check, async
      if (how === "detectChanges") c.cdr.detectChanges(); // checks this view now, sync
      const now = c.shown();
      setTimeout(() => this.write(`${how}: field=${c.count}, DOM right after="${now}", DOM later="${c.shown()}"`), 20);
    });
  }

  toggle(c: PlainCounter) {
    this.attached ? c.cdr.detach() : c.cdr.reattach();
    this.attached = !this.attached;
    this.write(this.attached ? "reattached" : "detached");
  }

  write(line: string) { this.log.add(line); }
}

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 83 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 handle micro-frontend architecture in Angular using Module Federation?

Continue