In an Angular @for over rows re-fetched from an API as new objects, what does track row cost compared with track row.id?
answer
- new objects every refresh
- identity keys never match
- destroy and recreate every row
- NG0956 in the console
- key reuse updates bindings only
basics
~20 sWith track row, every refetch yields new object references, so no key matches and Angular destroys and recreates every row view and its DOM; with track row.id, rows are matched by id and only their changed bindings update.
solid answer
~40 sThe `track` expression gives Angular the key it uses to match items to existing embedded views. `track row` uses object identity, and a refetch or an immutable update returns new objects, so every key is new: Angular destroys all row views (DOM nodes, directives, child components) and builds them again, losing focus, text selection and component state. In development it warns with `NG0956` when identity tracking re-creates the whole collection. `track row.id` matches rows across refetches, so Angular reuses the views, moves them if the order changed, and only updates bindings whose values differ. `track $index` reuses by position, which is cheap but ties row state to the slot, not the item. Duplicate keys trigger `NG0955` and slower reconciliation.
code
html · 11 lines<!-- every refetch rebuilds every row -->
@for (row of rows(); track row) {
<app-price-row [row]="row" />
}
<!-- rows matched by id: views reused, bindings updated -->
@for (row of rows(); track row.id) {
<app-price-row [row]="row" />
} @empty {
<p>No prices yet.</p>
}go deeper
Recall that the track key tells Angular which row is which, and that a stable id avoids rebuilding rows.
Explain reconciliation by key: identity keys never match on refetched or immutably updated data, so every view is destroyed and recreated.
Diagnose churn from symptoms such as lost focus, reset row state and NG0956, and pick id, index or identity keys by data shape.
Push stable ids into the data contract and view-model mapping so every list in the codebase can track cheaply and correctly.
## What track controls An `@for` block renders one **embedded view** per item: its DOM nodes, the directives on them, any child components and their state. When the collection changes, Angular **reconciles** the old list of views against the new items, and the `track` expression supplies the **key** used to decide "this item is the same one as before". (Since v17 the block requires `track`; the older `*ngFor` used `trackBy`, defaulting to identity.) What the key buys is the difference between two very different amounts of work: - **Key matches**: reuse the existing view, move it if its position changed, and update only the bindings whose values differ. - **Key is new**: create a view from scratch, run its component constructors and lifecycle hooks, and insert DOM. - **Key disappeared**: destroy the view, run its destroy hooks, and remove DOM. ## Why identity tracking churns on refetched data `track row` compares items by **reference** (`===`). That works while the same objects stay in the array. It breaks with two very common patterns: 1. **Refetching** from an API: the JSON is parsed into brand-new objects every time. 2. **Immutable updates**: `rows.map(r => ({ ...r, done: true }))` produces new objects even for unchanged rows. In both cases no key survives, so Angular tears down **every** row and builds every row again. For a table of a few thousand rows with child components this is a large, visible stall, and it has user-facing side effects: - focus and caret position in an input inside a row are lost; - text selection disappears; - row component state (an expanded panel, a local form) resets; - enter and leave animations can fire for rows that did not really change. Angular's development build detects this case. When an identity-tracked `@for` re-creates the entire collection and the rows are expensive to create, it logs a console warning with code **`NG0956`**, "Tracking expression caused re-creation of the DOM structure". It is a development-only warning, not an error, so production gives no hint. The source applies three conditions before warning: - the loop tracks by the item itself (`track row`), not by a property or `$index`; - the reconciliation re-created every item in the collection, not just some; - the row views are judged expensive to recreate: a row that is only one bound text node does not trigger it. The absence of the warning therefore does not prove a list is healthy; lost focus or reset row state in the running app is often the first clue. ## Choosing a key | Track expression | Matches items by | Cost on refetch or immutable update | Where state stays | |---|---|---|---| | `track row` | Object reference | Every row destroyed and recreated | Lost | | `track row.id` | A stable unique id | Views reused, only changed bindings updated | With the item | | `track $index` | Position | Views reused by slot, bindings updated | With the position, not the item | Guidance that follows from the table: - Use a **stable unique id** from the data (`id`, `sku`, `uuid`). If the data has none, add one when you map the response. - `track $index` is acceptable for **static** lists or lists that never reorder and hold no per-row state. On a sortable or filterable table, row state (an open menu, a focused input) stays in slot 3 while the item in slot 3 changes. - The key must be **unique**. Duplicate keys make Angular log **`NG0955`** in development, force a slower internal lookup, and can make it pick the wrong view when moving or removing rows. - Keep the expression **cheap**: it is evaluated per item on every reconciliation. A property read is ideal. If you track through a function, call it (`track keyOf(row)`); an uninvoked function reference is flagged by extended diagnostic `NG8115`. ## What @for does when the key matches but the object changed With `track row.id`, a refetched row is a new object with the same key. `@for` reuses the view and updates its context to the new object, so bindings (including child component inputs) receive the new values. That is cheap, and it is the point: DOM nodes and component instances survive, and only changed text or attributes are written. ## Interview framing The mechanism is not "Angular re-renders the list". It is "Angular reconciles views by key, and a key that never matches turns an update into a full rebuild". Identity tracking is the default mental trap because it looks correct in demos with mutable arrays and only fails once the data becomes immutable or comes from the network.
- When is track $index a reasonable choice, and when does it cause bugs?It suits static lists or lists that never reorder, insert in the middle, or hold per-row state: views are reused by position with no key lookup. On a sortable or filterable table the view in slot 3 keeps its component state and focus while a different item moves into slot 3, so an expanded panel or half-typed input appears on the wrong row.
- Does NG0956 appear in production builds, and does it fire for every identity-tracked list?No to both. It is a development-mode console warning, and it fires only when an identity-tracked `@for` re-created the entire collection and the row views are expensive to recreate. Production builds skip the check, so the churn is silent there.
saying these in an interview costs you the question
- Tracking by object identity is the safest default for API data
- track $index keeps row state attached to the right item after sorting
- NG0956 is a compile error that blocks the build
- Duplicate track keys only cost a little performance, never correctness
- Reusing a view by key means its child inputs never update