3 min readRishi

React List Keys That Survive a Filter

React List Keys That Survive a Filter

key={index} looks fine until the list changes shape. React uses the key to decide which component instance keeps its state. If the key is the position, then the state belongs to the position. Filter out row 0 and the state that was on row 1 is now on row 0. The checkbox stays ticked on the wrong person. The input keeps the previous row's draft.

This shows up in admin tables, in carts, and in any list with a local expanded row. It does not show up in a static map of strings, which is why the bug ships.

What the key has to be

The key is the identity of the item, stable for as long as that item is the same item.

SourceUse it when
Database id, UUIDThe row exists on the server
Client id created when the user added a draft rowThe row is not saved yet
A composite that cannot collide, such as ${orderId}:${lineId}The same line id can appear under two parents

Do not use the array index. Do not use a field the user can edit, such as the title, unless a title change is supposed to reset the row. Do not use Math.random() during render. A new key every render remounts the row, drops focus, and replays effects.

{rows.map((row) => (
  <OrderLine key={row.id} line={row} />
))}

If two rows can share an id because you concatenated pages and the API repeated a cursor item, fix the data. A duplicate key warning is React telling you the identity map is wrong. Suppressing it hides which row will keep the state.

Filter, sort, and prepend

These three operations are the test:

  1. Type into an input inside row B. Filter the list so row A disappears. Row B's input must still show what you typed.
  2. Sort by name. The same input must stay on row B, not on whoever is now in that slot.
  3. Prepend a new row. Existing rows must not remount. Effects that fetch on mount must not refetch.

Index keys fail all three. Id keys pass all three, as long as the component's state lives in the component that has the key. If the state lives in the parent, keyed by index in a parallel array, you have the same bug one level up. Keep the draft on the row object, or in a map keyed by row.id.

When the index is actually the identity

A fixed list of steps — "step 1, step 2, step 3" — that never reorders can use the index, because the position is the item. The moment you sort, filter, or insert, that stops being true. If you are unsure, use an id. The cost is nothing. The failure mode of index keys is a support ticket that says "the wrong account got the note."

Keep reading

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

  • No comments yet. Be the first.