You render a React list from an API response whose items have no id field. What do you use as the key, and why is `key={crypto.randomUUID()}` written inline in the JSX the wrong answer?
answer
- natural key, composite, minted id
- assign identity at the boundary
- render must be pure and repeatable
- new key every render means full remount
- index only for static, stateless lists
basics
~20 sDerive a key from fields that are stable and unique together, or mint an id once when the data enters the app and store it with the item. Generating one inline in JSX produces a new key every render, so React unmounts and remounts the whole list each time.
solid answer
~50 sWork down a ladder. First look for a natural key already in the payload — an email, SKU, slug, or ISO timestamp that uniquely identifies the row. Failing that, build a composite from fields that are jointly unique, e.g. `` `${r.userId}-${r.date}` ``. If the data genuinely has no identity, mint one **once**, at the point the response enters the app — in the fetch/normalise step or the reducer that stores it — and keep it on the item for as long as it lives. What you must not do is compute it during render: `key={crypto.randomUUID()}` runs on every render, so every key is new, React concludes every item was replaced, and it unmounts and remounts the entire list — destroying focus, typed values, scroll and animations, and paying full DOM cost on every update. The index is the fallback only for a list that never reorders, filters, or deletes from the middle and whose rows hold no state.
code
javascript · 6 linesexport async function loadRows(url) {
const res = await fetch(url);
const rows = await res.json();
// identity assigned once, where the data enters the app
return rows.map((row) => ({ id: crypto.randomUUID(), ...row }));
}go deeper
Know that a key must be the same value on every render for the same item, and that generating one inside the JSX breaks that rule.
Explain the ladder — natural field, composite, id minted at the data boundary — and why a fresh key each render makes React remount the whole list.
Show where identity is assigned in a real data flow, name what remounting actually costs the user (focus, typed input, scroll, restarted animations, re-run per-row fetches), and state the narrow contract under which the index is defensible.
Push the problem upstream: treat missing stable ids as a data-modelling defect, since client-minted ids cannot survive a refetch or a second client, and set a codebase convention for where identity is assigned.
## The ladder **1. A natural key in the payload.** Plenty of "no id" responses have one anyway: `email` for a user, `sku` or `isbn` for a product, `slug` for an article, `date` for a per-day row, `symbol` for a ticker. Ask whether the field is unique in this collection and whether it can change while the row is on screen. An email is a decent key until your app lets people edit their email, at which point the row remounts on save. **2. A composite key.** When no single field is unique, combine the ones that are jointly unique: ```jsx {entries.map((e) => ( <Row key={`${e.userId}-${e.date}`} entry={e} /> ))} ``` Be explicit about the separator so that `12`+`3` and `1`+`23` cannot collide, and pick fields that are part of the row's identity rather than its content. Hashing the whole object (`JSON.stringify(row)`) is the tempting shortcut and is wrong: the key then changes whenever any field is edited, so editing a row remounts it. **3. Mint an id at the boundary.** If the data has no identity at all, give it one exactly once, where the data enters the app: ```js const withIds = (await res.json()).map((row) => ({ id: crypto.randomUUID(), ...row, })); setRows(withIds); ``` Now the id lives in state alongside the row and is stable for the item's whole lifetime. The same applies to items the user creates locally: assign the id in the event handler or reducer that appends the row, not in the component that renders it. ## Why generating in render is a bug Render runs many times — on every state change in this component or an ancestor, and in development React may render again to surface impure code. A key computed during render is therefore a *different value every time*: ```jsx {rows.map((row) => <Row key={crypto.randomUUID()} row={row} />)} // never do this ``` Every new key is absent from the previous children and every old key is absent from the new ones, so React's conclusion is that the entire list was removed and a completely different list was added. The consequences: - every `Row` instance is unmounted and a fresh one mounted, so all local state resets and every mount effect re-runs — including any per-row fetch; - every DOM node is destroyed and rebuilt, so focus is lost mid-typing, uncontrolled input values vanish, scroll position resets and CSS transitions restart; - you pay maximum DOM cost on every single render, which is exactly the opposite of what keys are for. `Math.random()`, `Date.now()` and a module-level counter incremented in render all fail the same way, and they additionally make render impure, which breaks the assumptions React relies on to render safely. ## When the index is acceptable The index is a valid key only under a narrow contract: the list is never sorted, filtered, or inserted into except at the end; items are never removed from the middle; and the rows hold no state React does not rewrite from props — no uncontrolled inputs, no local `useState`, no focus or animation that has to follow the item. A hardcoded nav array, a static legend, a read-only table that is replaced wholesale — fine. Write it down as a comment if you rely on it, because the contract is invisible and the next person adds sorting. ## Deciding, in practice A short checklist you can say out loud: 1. Is there a field that identifies the row and does not change while it is displayed? Use it. 2. Can I make one from two or three such fields? Use a composite with an explicit separator. 3. Otherwise, where does this data enter the app? Add an id there and store it. 4. Only if the list is provably static and stateless, fall back to the index. 5. Never compute the key during render. ## The upstream conversation Worth saying at senior level: "the API returns rows with no identity" is usually a data-modelling gap, not a rendering problem. Client-side ids are invisible to the server, so they cannot survive a refetch — a refetched list gets brand-new ids and remounts. If the list is editable, paginated, or reconciled against server updates, the durable fix is to ask for a stable id in the response rather than to keep patching identity in the view layer.
- Why not just use JSON.stringify(row) as the key?Because it keys on content rather than identity. Any edit to any field produces a different key, so React unmounts the row and mounts a replacement — losing focus, local state and animations on exactly the row the user is editing. It is also expensive per row and unstable across key order in the object. Key on the fields that identify the row, not on the whole row.
- Client-minted ids fix the render, but what breaks when the list is refetched?The refetch produces fresh objects with brand-new generated ids, so React sees an entirely new list and remounts every row even though the data is largely unchanged. If you must mint client-side, reconcile new responses against existing items by a natural key and carry the old id over — or, better, get a stable id from the server, since that is what identity across requests actually requires.
- The list is static today, so is the index really a problem?Not today, and that is the trap: the contract that makes it safe — never sorted, never filtered, nothing inserted or removed in the middle, no per-row state — is invisible in the code. It gets broken by a feature request months later and the resulting bug looks like a data bug. Using a real key costs nothing and removes the landmine.
saying these in an interview costs you the question
- Just use a random UUID as the key.
- Any unique value works, stability does not matter.
- JSON.stringify the row and use that.
- Use the index; the list is small.
- Client-generated ids are as good as server ids.