An array of elements is a valid child, so a list is just .map(). Each element in that array needs a key: a string or number unique among its siblings that stays attached to the same piece of data across renders. React 7,897 uses keys to match the previous render's elements against the new ones; without one it falls back to position and logs Each child in a list should have a unique "key" prop in development.
The key belongs on the outermost element the callback returns, and it is not a prop — key never appears in props, so a component that needs the value must also receive it under another name. Keys are scoped to one parent, so two different lists may both use key="1".
<div id="root" style="font:15px system-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script type="text/babel" data-type="module">
import React from 'https://esm.sh/react@19.3.0';
import { createRoot } from 'https://esm.sh/react-dom@19.3.0/client';
const rows = [{ sku: 'KB-01', name: 'Keyboard', qty: 3 },
{ sku: 'MN-14', name: 'Monitor', qty: 0 },
{ sku: 'CB-07', name: 'Cable', qty: 12 }];
const cell = { border: '1px solid #888', padding: '3px 10px' };
createRoot(document.getElementById('root')).render(
<table style={{ borderCollapse: 'collapse' }}>
<tbody>{rows.filter(r => r.qty > 0).map(r =>
<tr key={r.sku}>
<td style={cell}>{r.sku}</td><td style={cell}>{r.name}</td>
<td style={{ ...cell, textAlign: 'right' }}>{r.qty}</td>
</tr>)}</tbody>
</table>
);
</script>
filter runs before map, and the surviving rows keep their identities because the key comes from the data, not the position. Had the key been the array index, dropping a row would make React reuse row 0's DOM node for what used to be row 1 — harmless for plain text, but it carries over uncontrolled input values, scroll positions, animations and focus. That is the classic "my form fields swapped" bug.
| Key source | Safe? | Why |
|---|---|---|
| Database id, SKU, UUID | Yes | Stable and unique per item |
| crypto.randomUUID() in render | No | New every render; remounts everything |
| Array index | Append-only lists | Breaks on insert, delete or sort |