Home/Rows/Row Identity
Rows

Row Identity

How the grid recognises the same row across two different objects. Name the field with trackBy and a targeted update lands on the row you meant; leave it out and the call quietly changes nothing. When no single field is unique, trackBy takes a function instead.

trackBy as a property name — and what happens without it

One button, two grids, the same transaction sent to both. The left one names its id field and the row's salary moves; the right one names nothing and reports zero rows updated. Same data, same call.

Press it and read the two lines underneath.

trackBy: 'id'

#
First name
Department
Salary
1
Alice
Engineering
$40,000
2
Ben
Marketing
$43,500
3
Carla
Support
$47,000
4
Dmitri
Legal
$50,500
5
Elena
Operations
$54,000
6
Farid
Engineering
$57,500
7
Greta
Marketing
$61,000
8
Hana
Support
$64,500
1–8 of 20

Last transaction:Press the button above.

no trackBy

#
First name
Department
Salary
1
Alice
Engineering
$40,000
2
Ben
Marketing
$43,500
3
Carla
Support
$47,000
4
Dmitri
Legal
$50,500
5
Elena
Operations
$54,000
6
Farid
Engineering
$57,500
7
Greta
Marketing
$61,000
8
Hana
Support
$64,500
1–8 of 20

Last transaction:Press the button above.

Two grids, the same eight rows, the same button. The only difference between their configs is one key:

trackBy: 'id'      // left
trackBy: undefined // right

The button reads the first row out of each grid, spreads it into a new object with a bigger salary, and hands that to applyTransaction({ update: [row] }). The left grid finds the row it belongs to by its id and repaints it. The right grid has no field to match on, compares by reference, decides it has never seen this object, and reports 0 rows updated.

Nothing throws. That is the whole reason this page exists: the failure is a call that succeeds and does nothing, which is the hardest kind to notice — and it is guaranteed the moment your data comes over HTTP, because the parsed objects are never the ones you had before. In development the grid does log one console warning naming trackBy, so the failure is not completely silent; it is just easy to scroll past, because everything that ran reports success.

trackBy as a function — when no single field is unique

These rows have no id. A licence is identified by its region and its code together, and neither is unique on its own, so trackBy is a function that builds the key.

The composite key is what finds that one row.
Region
Code
Seats
Expires
EMEA
A-100
250
2026-03-31
EMEA
B-200
120
2026-06-30
EMEA
C-300
40
2025-12-31
AMER
A-100
500
2026-09-30
AMER
B-200
310
2026-01-31
APAC
A-100
90
2025-11-30
APAC
C-300
175
2026-08-31
1–7 of 7

Last transaction:Press the button above.

These licences have no id. EMEA appears three times, A-100 appears three times, and only the pair identifies a row — so trackBy is a function:

trackBy: (_index, row) => `${row.region}:${row.code}`

The signature is (index: number, row: T) => any, the same shape Angular's own trackBy uses. Use the row, not the index: an index is a position, and a position changes the moment anything is sorted, filtered or paged, which is exactly when a targeted update is most likely to land on the wrong row.

The button renews EMEA / A-100 — the first row, and one of three rows sharing each half of its key. Only the combined key picks it out; a trackBy: 'code' would have matched the AMER and APAC rows too.

Anything stable works — a composite string, a hash, a compound number. The one rule is that the same row must produce the same key every time it is asked, including after a round trip through your API.

The problem it solves

The grid regularly holds row objects that are not the ones your code is holding. Anything that came over HTTP was parsed into fresh objects; anything you edit produces a new object rather than mutating the old one. Comparing by reference then says "I have never seen this row" about a row the reader is looking at.

trackBy is how you tell the grid what makes a row that row. Give it the field that identifies one, and it can match your object to the one on screen. Without it, the identity falls back to the row object itself — correct, but true of almost nothing.

What stops working without it

  • Targeted updates match nothing. applyTransaction() finds rows to update or remove by id. With no trackBy, an update applies to zero rows and a removal removes none — which is exactly what the demo above shows, along with the count the call hands back. The grid also logs one console warning rather than failing silently.
  • Measured row heights are re-measured. With content-driven heights the cache is keyed by row id, so without one every reload starts from scratch. See Auto Row Height.
  • Anything you build on row identity. Your own code usually needs the same answer — which row was clicked, which rows are pending, which row a websocket message is about. Deriving it once in trackBy keeps your answer and the grid's the same one.

None of these throw. That is what makes the option worth a page: the failure mode is a feature quietly doing nothing, which is the hardest kind to notice and the cheapest kind to prevent.

Reloading the grid — a new data source, a sort, a filter — clears selection and open panels regardless of trackBy, because the grid treats it as a fresh set of rows. The option is about matching a row you hand back to the grid, not about surviving a reset. Use Grid State to restore what a reader had before.

Two forms

// A property name — the common case, and enough whenever your rows carry a unique field.
trackBy: 'id'

// A function — for a composite key, a nested id, or an id you have to derive.
trackBy: (index, row) => row.tenantId + ':' + row.id

The function form receives the row index as well, but do not use it: an index is a position, not an identity, and it changes the moment anything is sorted, filtered or paged — which reintroduces the exact problem the option exists to solve.

What makes a good id

  • Unique across every row the grid can hold, not just the current page. Two rows sharing an id are treated as one.
  • Stable — the same row keeps the same id across reloads and edits. An id derived from a value the reader can edit is not stable.
  • Primitive — a number or a string. Ids are compared by value, so an object works only if it is literally the same object, which puts you back where you started.

Replacing your data, or changing it in place

There are two ways to get new values into a grid, and they are not the same thing. Replacing means handing the grid a fresh array — a new fetch, a reload, an immutable state update. Changing in place means pushing individual rows through applyTransaction() and leaving the rest of the data alone.

Both are supported, and trackBy is what makes either survive. When you replace the data, every row object is new; without an id the grid has no way to recognise them as the rows it was already showing, so selection, expansion, focus and measured heights are discarded along with the objects. With an id, the same rows carry all of it across the swap and only what actually changed re-renders.

When you change rows in place, trackBy is not a nicety at all — it is the lookup. applyTransaction({ update }) and { remove } find their target by row id, so without one they match nothing and return an empty updated array. The call itself succeeds — nothing throws, nothing changes on screen, and the only sign is a development-mode console warning and a count of zero in the result you were probably not reading.

Which to reach for is a question about your application, not about the grid. Replacing is simpler and fits an immutable store; changing in place is what you want when updates arrive one at a time and re-rendering the whole set would be wasteful. Give rows a stable id and you are free to do either — or both, on the same grid.

Setting it once

If your rows all carry the same id field, set it for the whole app rather than per grid:

// app.config.ts
providers: [
  provideGridDefaults({ trackBy: 'id' }),
]

A grid whose rows use a different field still overrides it locally. See Global Configuration.

Row identity options

PropertyTypeDefaultDescription
config.trackBykeyof T | ((index: number, row: T) => any)the row object itselfHow the grid identifies a row across two different objects. A property name for the common case, or a function for a derived or composite id.
grid.applyTransaction()(tx: { add?, update?, remove? }) => TransactionResult—Pushes a change into a rendered grid. update and remove match by row id, so both are no-ops without trackBy. See Live Updates.
TransactionResult.updatedUpdatedRow<T>[]—What the call actually changed. An empty array after an update you expected to land is the symptom of a missing trackBy.
config.autoRowHeightbooleanfalseMeasured heights are cached per row id. See Auto Row Height.
provideGridDefaults({ trackBy })Partial<GridConfig>—One id field for every grid in the app; a single grid can still override it.