Why does Angular `@for` recreate rows when you reuse object identities incorrectly?
Angular's `track` expression decides which DOM row belongs to which item. See why unstable keys recreate rows, how to preserve input state, and where the fix stops helping.
A list can look correct and still be wrong in a way that matters. If a row contains an input, a focus ring, or any other stateful control, then a list refresh that recreates the row will drop that state even when the visible labels are unchanged. Angular's `@for` block makes that decision explicit through `track`, which means the article is really about item identity, not just syntax. The example below uses the Angular docs' tracking model and a controlled fixture to show how the wrong key turns a reorder into a DOM replacement. The point is not that Angular is fragile; the point is that every keyed renderer has to pick whether it values position or persistent identity.
Reproduce the row that loses state after a reorder.
The row uses the wrong identity key, so Angular cannot match the new data to the old DOM view.
Editing the second row's input, then reordering the array, should either preserve the value or reset it depending on the `track` expression.
That difference matters because the DOM node is where the user left a value, focus, selection, or an in-flight IME composition. If Angular decides the node is a different row after the reorder, the browser has no reason to keep those stateful details attached to the same item.
@for (item of items; track item) {
<li>
<input [value]="item.label">
</li>
} SourcesAngular @for API (opens a new tab)Angular NG0956 (opens a new tab)
Explain why Angular associates views by key, not just by position.
Angular documents that the `track` expression determines the key used to associate array items with DOM views, and that a poor choice can re-create the entire DOM structure.
Two list objects with the same content but new object identities should still map to the same row only when the key is stable.
That is the real distinction between a template that merely looks correct and one that remains correct after an immutable refresh. A position-based key works only while the item order never changes. Once the list can sort, filter, paginate, or rehydrate from the server, the key has to describe the item itself rather than the slot it happened to occupy on the previous render.
SourcesAngular @for API (opens a new tab)Angular templates essentials (opens a new tab)
Change the `track` expression to a stable key.
Use the unique property that survives reloads and reorder operations, such as an item id.
`track item.id` keeps the same row instance when the list is sorted differently or cloned from fresh data.
This is a tiny code change, but it changes ownership. The row now belongs to the identity of the item, not the accidental order in the current array. That is why the same browser state survives a reverse, a resort, or a full replacement with fresh objects that carry the same ids.
@for (item of items; track item.id) {
<li>
<input [value]="item.label">
</li>
} Prove the repaired behavior with a controlled browser test.
The test checks the value and focus state before and after a reorder under the broken and repaired keys.
The broken case resets the edited row; the fixed case preserves it.
The browser check is important because a compile-time template check cannot see DOM reuse. Both versions would still render the same visible list labels. Only the runtime test can show whether the value stays attached to the logical row or drifts with the physical position.
Render the broken case
Mount the list with the unstable key, type into the second input, and reorder the array.
Observe the failure
The edited value disappears or moves to the wrong row because the DOM node was recreated.
Render the repaired case
Switch to the stable id key and repeat the same reorder at the same viewport.
Verify the state survives
The same edited input keeps its value and focus because Angular reuses the row instance.
Know the limits of the fix.
Stable tracking preserves DOM reuse, but it does not solve data races, stale server responses, or incorrect component state management.
If the list is repopulated with different business entities that happen to share a label, the key still needs to come from the real identity field.
There are two useful edge cases to keep in mind. First, `$index` can still be acceptable for a truly static list, such as a short fixed menu whose order never changes. Second, a stable key is not an excuse to skip data validation: if two records share the same id, Angular will faithfully preserve the wrong association. The identity rule only works when the source data is itself trustworthy.
SourcesAngular @for API (opens a new tab)Angular NG0956 (opens a new tab)
Compare the broken and fixed cases directly.
The broken case still renders three rows, which is why this bug can hide in plain sight during casual review. The user sees the same labels, but the browser state has been attached to the wrong row positions.
The fixed case also renders three rows, but now the label, value, and row identity move together. That is the useful mental model: the DOM node is a carrier for transient browser state, and the `track` expression decides whether Angular keeps or replaces that carrier.
This is also why the article does not claim every reorder is dangerous. If a row is purely informational and has no local state, a weak key may be less visible. Once the row contains inputs, selection, expand/collapse state, or any other user-owned browser state, the difference becomes operationally important.
The browser test makes the distinction visible without depending on screenshots or hand-wavy inspection. A failed row identity decision should change the observable pairings between labels and values, and the test checks those pairings directly.
That gives the reader a concrete stop condition: if the same item keeps its value after a reverse, the key is stable enough for this shape of data.
SourcesAngular @for API (opens a new tab)Angular templates essentials (opens a new tab)
