skip to content

What properties must a list key have to be usable, and which common key sources fail them?

level: middleimportance: should knowfreq 66%

answer

  1. identity comes from the data
  2. stable, unique, cheap
  3. unique among siblings, not globally
  4. generated during render is worse than an index

basics

~20 s

A key must come from the data, stay the same for an item across updates, be unique among its siblings, and compare cheaply. The index, a value generated while rendering, a duplicated field and an editable field each fail one.

solid answer

~50 s

Four properties. It is **from the data**, so it describes the item rather than where the item sits. It is **stable**: the same item carries the same key across updates, reorders and refetches. It is **unique among siblings** — the only scope pairing compares in, so two unrelated lists may both use `1, 2, 3`. And it is **cheap to compare**, so a plain string or number. The failures follow: an index is the position under another name; a value generated during the update makes every item look new, so the list is rebuilt every time; a repeated field makes pairing ambiguous; an editable field such as a display name means typing into a row changes its key and discards that row's instance mid-edit. With no natural id, compose one from fields that jointly identify the item, or mint an id when the item is created.

go deeper

for a junior

Memorise the shape of a good key: an id that belongs to the item, the same every time, different from its siblings. If you are reaching for the index, that is the signal to go looking for an id.

for a middle

Be able to rank the bad sources and say why each fails: index equals position, generated equals rebuild-everything, duplicated equals ambiguous pairing, editable equals discard-mid-edit. Name sibling scope as the actual uniqueness requirement.

for a senior

In review, check where the key comes from rather than that one exists. Push identity upstream into the payload, mint client-side ids at creation, and verify composite keys are unique with a development-time check instead of an assumption.

for a principal

Treat item identity as an interface contract across teams: every collection that crosses a boundary carries a stable id. That removes the most common cause of index keys and stops each consumer inventing its own rule.

## What the pairing step needs from you When a runtime updates a list, it pairs the children it built last time with the children described now, and a key is the identity it uses to do that. Four properties make a value fit for the job. 1. **It comes from the data.** A key answers *which item is this*, so it has to be a property of the item — a record id — not a property of the render, such as where the item happens to sit this time. 2. **It is stable.** The same item carries the same key on every update, through reorders, filters, pagination and refetches. If a key changes, the runtime is being told the item was replaced. 3. **It is unique among its siblings.** The comparison only ever happens inside one parent's children, so uniqueness is required there and nowhere else. 4. **It is cheap and simple to compare.** Runtimes generally use a plain equality check, so a string or a number behaves predictably. ## The common sources, and how they score | Key source | Verdict | What goes wrong | |---|---|---| | A stable id carried by the record | Correct | nothing — this is the intended source | | A composite of fields that jointly identify the item | Correct if truly unique and stable | breaks the moment one of those fields becomes editable | | The item's index in the list | Fails *from the data* | it is positional matching under another name, and it silences a missing-key warning | | A value generated while rendering | Fails *stable* | every key is new every update, so nothing pairs | | A field that can repeat, such as a category label | Fails *unique among siblings* | ambiguous pairing: a row silently missing, or two rows sharing one instance | | An editable field, such as a display name | Fails *stable* | editing the field changes the key, discarding the row's instance mid-edit | | A freshly built object or array | Fails *cheap to compare* | it compares unequal each update, behaving like a generated key | ## Why a generated key is worse than an index An index at least pairs something: rows keep their instances, and the bug is that the *wrong* rows keep them. A key minted during the update pairs nothing. Every new key is unknown, so every child is created, and every old key has vanished, so every instance is destroyed. On every single update the list is rebuilt from nothing: - all per-row state resets, including scroll positions and expanded sections; - focus and any in-progress text entry are lost, so a user cannot finish typing; - host nodes are recreated rather than moved, so entry animations replay on rows that never moved; - teardown and setup work runs for every row, which may mean re-subscribing or re-requesting; - the runtime's own skip-work strategies cannot help, because nothing is recognised as unchanged. It looks like a fix — the warning goes away and every value is unique — and it is the most expensive mistake in this area. ## Sibling scope, stated precisely Keys are compared inside one sibling list. That means two separate lists on the same screen may each use `1, 2, 3` with no interaction, a nested list has its own scope independent of its parent's, and the same key value appearing in a different part of the tree is not a conflict. It also means a rule mandating globally unique keys is over-constraint: it teaches the wrong requirement and pushes teams into synthesising keys where a plain id would do. ## When the data has no identity This is where index keys actually come from — not laziness, but a payload with nothing to key by. The options, in order of preference: 1. **Get one into the data.** Ask the producer for an id. Identity is a property of the entity, so this is the correct place to solve it once for every consumer. 2. **Compose one.** Combine fields that together identify the item and are not user-editable. Verify uniqueness in development rather than assuming it. 3. **Mint one at creation.** For rows that exist only on the client — an empty row a user just added — generate an identifier **when the row is created** and store it alongside the row's data. That is stable, because it is created once, not once per update. Generating it during the render is the failure above. 4. **Accept the index** only for a list that is stateless and never changes shape except at the end, and write down why. ## The one-line test Ask: *if this list were shuffled, would each key still point at the same item?* An id passes. An index does not. A name the user can edit passes the shuffle but fails the edit — which is why the property to check is stability over the whole life of the item, not just over one reorder.

  • What do you key by when the data carries no identifier at all?
    Prefer getting an id into the payload, since identity belongs to the entity. Otherwise compose a key from fields that jointly identify the item and cannot be edited, and verify uniqueness in development. For rows created on the client, mint an identifier at creation time and keep it with the row's data — once per row, never once per render.
  • Does a key have to be a string or a number?
    In practice yes. Runtimes pair keys with a simple equality check, so a value rebuilt on every update — a fresh object or array — compares unequal each time and behaves exactly like a generated key: everything is destroyed and rebuilt. A stable primitive keeps the comparison cheap and the pairing predictable.
  • Is it a problem if two different lists on one screen use the same key values?
    No. Pairing compares one parent's previous children against its new children, so the key space is the sibling list. Reusing `1, 2, 3` in an unrelated list, or inside a nested list, cannot collide. Requiring global uniqueness adds work and hides the real rule.

saying these in an interview costs you the question

  • Uses the array index because every item already has one
  • Generates a fresh identifier per item during rendering
  • Keys a row by an editable field such as a display name
  • Demands keys unique across the entire application
  • Assumes duplicate keys are harmless when rows look right
  • Treats a missing-key warning as style rather than correctness