Lists that melt your app
Lists fail in two ways: wrong keys make state “jump” between rows, and rerender cost explodes when each keystroke redraws the whole list. This produces subtle UX bugs and typing lag.
Break the invariant: unstable identity + full rerender
⚠️ Repro steps
- Type into "Row 1" and "Row 3".
- Click "Insert at top".
- Observe: typed values may appear in the wrong rows (identity broke).
- Now type again: notice the rerender counter spikes (performance broke).
Status
INVARIANT INTACT
Flips
0
Parent renders
0
Work (simulated)
3699
Detected issue
Status:Invariant intact
INVARIANT INTACT means the bug hasn’t been reproduced yet. Insert at top at least once to trigger the identity break.
Row 0
key = index
Row 1
key = index
Row 2
key = index
Row 3
key = index
Row 4
key = index
Row 5
key = index
Row 6
key = index
Row 7
key = index
Row 8
key = index
Row 9
key = index
Row 10
key = index
Row 11
key = index
Row 12
key = index
Row 13
key = index
Row 14
key = index
Row 15
key = index
Why this breaks
- Keys define identity. React matches previous children to next children by key.
- If you use index as key, insert/remove shifts indices → React reuses the wrong row instance.
- That breaks the invariant: “row state belongs to the same logical item across renders.”
- If the parent re-renders the whole list on every keystroke, work becomes O(N) per character.
- Identity (keys) and update isolation (memo) are orthogonal. You need both to prevent list-wide re-renders.
Reference (correct mental model)
Incorrect / Buggy
// ❌ Index keys: identity is position, not the item
items.map((item, i) => <Row key={i} item={item} />)// ❌ Parent owns and re-renders all rows per keystroke
setItems(prev => prev.map(...))Correct
// ✅ Stable keys: identity follows the item
items.map((item) => <Row key={item.id} item={item} />)// ✅ Memo rows + stable callbacks to isolate updates
const Row = memo(RowView)
const onChange = useCallback((id, next) => { ... }, [])Fix
This fix DOES change the UI behavior (values stop jumping and counters stabilize), so the fixed preview is shown.
Where the fix lives
Key takeaways
- Keys are identity. Never use index if the list can reorder/insert/remove.
- State jumps are an identity bug, not a “controlled input” bug.
- Typing lag often means “too many components re-render per keystroke.”
- Stable keys + memoized rows (+ virtualization for large lists) form the baseline pattern.
Approach & tradeoffs
Approach
- Give each row a stable id at creation time (not during render).
- Use that id as key so identity survives inserts/reorders.
- Memoize rows and keep props stable to avoid full-list rerenders.
- Measure simple counters so regressions are obvious.
Tradeoffs
- Memoization adds complexity: you must ensure stable props and avoid accidental changes.
- Virtualization improves performance but changes UX (scroll behavior, find-in-page, variable heights).
- For small lists, perf work may be premature — but stable keys are always correct.
Common pitfall