You type a note in the first row of a list. Then you sort the list. The note stays in the first row, but that row now shows a different record.
This can happen when a React list uses its array index as a key. The rows look correct until someone edits a field and changes the order.
Why the text ends up in the wrong row
Each editor below stores its text with useState. React uses the key to decide which editor instance belongs to which item.
An index key identifies a position: first row, second row. After sorting, a different record can occupy that position while the editor keeps its previous state. A record ID gives React a way to match the editor to the same record after it moves.
Try the example
Paste this component into a React application's App.jsx. Start with the default stable keys, edit a note, and reverse the list. The note should follow its record.
import React, { useState } from 'react';
const initialDrafts = [
{ id: 'incident-42', title: 'Cache invalidation review', body: 'Check the stale key path.' },
{ id: 'incident-57', title: 'Retry policy review', body: 'Check the backoff limits.' },
];
function DraftEditor({ draft, position, stableKeys }) {
const [body, setBody] = useState(draft.body);
return (
<article aria-label={`Draft at position ${position + 1}`}>
<h2>{draft.title}</h2>
<label>
Reviewer notes for {draft.id}
<textarea value={body} onChange={event => setBody(event.target.value)} />
</label>
</article>
);
}
export default function ReviewQueue({ stableKeys = true }) {
const [drafts, setDrafts] = useState(initialDrafts);
const reverseQueue = () => setDrafts(current => [...current].reverse());
return (
<main>
<p>Key strategy: {stableKeys ? 'draft id' : 'array position'}</p>
<button type="button" onClick={reverseQueue}>Reverse review order</button>
{drafts.map((draft, position) => (
<DraftEditor
key={stableKeys ? draft.id : position}
draft={draft}
position={position}
stableKeys={stableKeys}
/>
))}
</main>
);
}
To see the bug, render the component with stableKeys={false}. Edit incident-42 before reversing the list. The first row will now display incident-57 while keeping the text you entered for incident-42.
The important line is:
key={stableKeys ? draft.id : position}
In a real editable list, use a stable value that represents the item among its siblings. Avoid generating a new random key during every render: that gives the component a new identity and resets its state.
What keys do not save
If you remove a row from the tree, its local state is discarded. Bringing it back with the same key does not restore the old text. If edits must survive filtering or navigation, store them in a parent or another persistent store, indexed by record ID.
For a single editor, you might deliberately want to clear the local buffer when the selected record changes. Keying that editor by the selected ID makes that reset explicit. Keep the key on the smallest subtree you intend to reset.
Test the interaction that caused the bug
Edit one record, reorder the list, and check that the edited value still belongs to that record. Checking only the displayed row order misses the problem.
Then remove the edited record and bring it back. Decide whether losing the local text matches your product's requirements. That is a persistence decision as well as a rendering decision.
Read more in React's state identity guide and list key guidance.
Top comments (0)