A user list using `.map`. No `key` prop. React falls back to index-based reconciliation. Looks fine in dev. In production with reordering or filtering, every change remounts every row.
The vulnerable diff
// components/UserList.tsx
export function UserList({ users }: { users: User[] }) {
return (
<div>
{users.map(u => (
// BUG: no key — React falls back to index-based reconciliation
<UserRow user={u} />
))}
</div>
);
}What is wrong
React uses the `key` prop to match children across renders and decide which to reuse, re-mount, or remove. Without an explicit key, React falls back to the index — which is stable only if the list never reorders or filters. As soon as you insert at the top, remove a middle row, or reorder, every row past the change point has a 'new' index and React thinks it is a different component. The mount/unmount cycle drops any internal DOM state (input value, scroll position, focus), runs cleanup effects, runs mount effects, paints. Measurable CPU on large lists; user-visible focus loss on small lists.
The attack
Reproducer:
# UserRow contains <input>. Type in row 3. Then a new user is added at the top.
# With no key: every row's <input> remounts, focus lost, typed text gone.
# With key={u.id}: only the new top row mounts; row 3 keeps focus and text.Effect on dev: subtle. Effect on production with active typing or scrolling: very visible.
Mesrai's review comment
mesraipilot · Bot · reviewed 30 sec ago
[mesrai] [code-review] [Performance] [React-Key] [medium]
`.map` returning JSX without an explicit `key` prop. React falls
back to index reconciliation — every reorder/insert remounts past
the change point, dropping DOM state.
Use a stable unique id from the data:
users.map(u => <UserRow key={u.id} user={u} />)
Never use the array index as the key for lists that reorder. Index
keys are equivalent to no key for reconciliation purposes.The fix
// components/UserList.tsx — fixed
export function UserList({ users }: { users: User[] }) {
return (
<div>
{users.map(u => <UserRow key={u.id} user={u} />)}
</div>
);
}Stable unique id from the data. React now matches rows correctly across renders, reuses mounted components, preserves DOM state.
Why human review missed it
Missing-key is one of the most-warned-about React bugs but the warning only fires when React notices in dev — and many code paths don't emit the warning. The bug is invisible in tests using single fixed-order data. Mesrai catches every `.map` returning JSX without a key, including the cases where lint and dev warnings stay silent.
Related rules + further reading
Mesrai rule pack: language/react-key-required — flags .map → JSX without explicit key.
React docs: Rendering Lists — Keys.
ESLint plugin react/jsx-key covers most but not all shapes.
Takeaway
Stable key per row. Always. From a unique data id, never the index.