Skip to the solution
MediumFrontend10 min read

Why does @for require track and how does @empty improve UX?

215 views

The problem

Question presented to candidate: "In Angular's @for block, track is mandatory, unlike the old trackBy. Why did the team make it required, what should you track by, and what does the @empty block give you?"

What a strong answer should cover:

  • track gives each item an identity, so Angular can match old rows to new rows and move or keep their DOM instead of destroying and recreating it.
  • Without a good key, a refetch that returns new objects recreates every row, losing focus, input text, scroll position, animations and child component state.
  • Good keys: a stable unique id (track item.id). track $index is only right for static lists; track item (identity) fails when objects are recreated.
  • Dev-mode diagnostics: NG0955 (duplicate keys) and NG0956 (the whole collection was recreated).
  • Making track mandatory fixes the most common performance mistake of *ngFor, where trackBy was optional and usually forgotten.
  • @empty renders when the collection is empty, null or undefined, so an empty state needs no separate @if and cannot drift out of sync with the loop.

Clarifying questions expected:

  • "Does the data come from an API that returns fresh objects on every call?"
  • "Can the list be reordered, filtered or edited in place?"

Code / implementation expected: Yes β€” a list showing the difference between a real key and $index, with an @empty state.

  • control-flow
  • track
  • performance

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

10 min read Β· 9 sections

Read the code

Angulartrack t.id vs track $index with per-row input state, plus @empty
import { Component, signal } from "@angular/core";

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

// Type a note next to "Write tests", then click "Done with first".
// The left list keeps the note with its task; the right list leaves it behind.
@Component({
  selector: "app-root",
  template: `
    <div style="display: flex; gap: 32px">
      <section id="good">
        <h4>track t.id</h4>
        @for (t of tasks(); track t.id) {
          <p>{{ t.title }} <input placeholder="note" /></p>
        } @empty {
          <p class="empty">All done. Nice work!</p>
        }
      </section>
      <section id="bad">
        <h4>track $index</h4>
        @for (t of tasks(); track $index) {
          <p>{{ t.title }} <input placeholder="note" /></p>
        } @empty {
          <p class="empty">All done. Nice work!</p>
        }
      </section>
    </div>
    <button (click)="tasks.update(list => list.slice(1))">Done with first</button>
  `,
})
export class AppComponent {
  tasks = signal<Task[]>([
    { id: 1, title: "Write tests" },
    { id: 2, title: "Fix bug" },
    { id: 3, title: "Ship it" },
  ]);
}

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 74 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 nextMedium

How has the Angular build changed with esbuild + Vite (ng build / ng serve)?

Continue