Skip to the solution
HardFrontend8 min read

What is the purpose of the KeyValueDiffers and IterableDiffers services?

1.0k views

The problem

Question presented to candidate: "A parent pushes items into an array and edits keys on a settings object, both passed as inputs to an OnPush child, and the child never notices. What are IterableDiffers and KeyValueDiffers, how would you use them here, and would you still design it this way in Angular 22?"

What a strong answer should cover:

  • Inputs are compared by reference, so in-place mutation does not fire ngOnChanges or mark an OnPush child; differs compare contents instead.
  • IterableDiffers (arrays, Sets, any iterable) and KeyValueDiffers (objects, Maps) are injectable factory registries: find(value).create(trackBy?) gives a stateful differ.
  • differ.diff(current) returns null when nothing changed, otherwise a change record with forEachAddedItem, forEachRemovedItem, forEachMovedItem, forEachIdentityChange (iterable) or forEachChangedItem (key-value).
  • Typical home: ngDoCheck, followed by markForCheck() or a signal write so an OnPush view refreshes; a trackBy function defines item identity.
  • Used internally by NgForOf, NgStyle and KeyValuePipe; the built-in @for uses its own reconciliation with track.
  • Modern alternative: immutable updates or signals make most manual diffing unnecessary; differs run on every check, so cost matters.

Clarifying questions expected:

  • "Can we change the parent to pass new references instead of mutating?"
  • "Is the child OnPush, and is the parent checked when it mutates?"

Code / implementation expected: Yes β€” a child that reports what changed in a mutated array and object using both differs.

  • performance
  • advanced

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

Read the code

AngularDetecting in-place mutations of an array and an object input with differs in ngDoCheck
import {
  ChangeDetectorRef, Component, DoCheck, Input, IterableDiffer, IterableDiffers,
  KeyValueDiffer, KeyValueDiffers, inject, signal,
} from "@angular/core";

type Task = { id: number; title: string };

@Component({
  selector: "change-report",
  template: `
    <p>{{ tasks.length }} tasks, theme {{ settings["theme"] }}</p>
    <ul>@for (line of report(); track $index) { <li>{{ line }}</li> }</ul>
  `,
})
export class ChangeReport implements DoCheck {
  // Plain @Input objects that the parent MUTATES in place: the reference never changes,
  // so ngOnChanges never fires. A differ compares contents instead.
  @Input() tasks: Task[] = [];
  @Input() settings: Record<string, string> = {};

  private cdr = inject(ChangeDetectorRef);
  private tasksDiffer: IterableDiffer<Task> =
    inject(IterableDiffers).find([]).create<Task>((_i, t) => t.id); // trackBy: identity is the id
  private settingsDiffer: KeyValueDiffer<string, string> = inject(KeyValueDiffers).find({}).create();

  report = signal<string[]>([]);

  ngDoCheck() {
    const lines: string[] = [];
    const t = this.tasksDiffer.diff(this.tasks);
    t?.forEachAddedItem(r => lines.push(`added "${r.item.title}" at ${r.currentIndex}`));
    t?.forEachRemovedItem(r => lines.push(`removed "${r.item.title}" from ${r.previousIndex}`));
    t?.forEachMovedItem(r => lines.push(`moved "${r.item.title}" ${r.previousIndex} -> ${r.currentIndex}`));
    t?.forEachIdentityChange(r => lines.push(`same id ${r.trackById}, new object "${r.item.title}"`));

    const s = this.settingsDiffer.diff(this.settings);
    s?.forEachAddedItem(r => lines.push(`key added ${r.key}=${r.currentValue}`));
    s?.forEachChangedItem(r => lines.push(`key changed ${r.key}: ${r.previousValue} -> ${r.currentValue}`));
    s?.forEachRemovedItem(r => lines.push(`key removed ${r.key}`));

    if (lines.length) {
      this.report.set(lines);   // a signal, so this OnPush view refreshes
      this.cdr.markForCheck();  // the plain-field bindings above need this too
    }
  }
}

@Component({
  selector: "app-root",
  imports: [ChangeReport],
  template: `
    <button id="add" (click)="add()">push a task</button>
    <button id="reorder" (click)="reorder()">reverse</button>
    <button id="rename" (click)="rename()">replace task 1 object</button>
    <button id="theme" (click)="theme()">toggle theme, drop lang</button>
    <change-report [tasks]="tasks" [settings]="settings" />
  `,
})
export class AppComponent {
  tasks: Task[] = [{ id: 1, title: "write" }, { id: 2, title: "test" }];
  settings: Record<string, string> = { theme: "light", lang: "en" };
  private next = 3;

  add() { this.tasks.push({ id: this.next, title: `task ${this.next++}` }); }
  reorder() { this.tasks.reverse(); }
  rename() { const i = this.tasks.findIndex(t => t.id === 1); this.tasks[i] = { id: 1, title: "write v2" }; }
  theme() { this.settings["theme"] = this.settings["theme"] === "light" ? "dark" : "light"; delete this.settings["lang"]; }
}

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 86 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

Signals signal()/computed()/effect() vs RxJS β€” when do you use which in Angular 18+?

Continue