A user deletes the second row of an editable list and the wrong row now shows the text they typed. What is going on?
State belongs to a position in the tree identified by key among its siblings, not to your data. With array indices as keys, deleting a row shifts every index, so React updates surviving components in place and their local state stays behind with the old slot.
What the interviewer is scoring
- Whether the candidate locates component state in a tree position identified by key rather than in the data
- Does the answer separate a remount from a re-render and say how they would establish which happened
- That index keys are accepted only for lists that never reorder, filter or insert
- Whether random or content-derived keys are rejected, with the reason rather than as folklore
- Does the candidate recognise that resetting state deliberately by changing a key is the same mechanism
Answer
What React is matching between two renders
Rendering produces a description of what the UI should be, and React's job is to work out how to turn the previous description into the new one with the fewest operations. For a list of children it does that by pairing up old and new children, and the pairing rule is that a child is matched to the one in the same parent with the same position, or the same key if keys are present. When two children are matched, React keeps the existing component instance and the existing DOM node and simply gives it new props. When it cannot match a child, it unmounts the old instance and mounts a new one.
The important consequence, and the thing this question exists to surface, is where local state lives. useState in a row component, the current value of an uncontrolled input, scroll position, focus, an open dropdown, a half-finished inline edit: none of that lives in your array of data. It lives on the instance React is keeping alive at that slot. Matching decides which instance survives, and the surviving instance brings its state with it.
Indices are the bug
If the list is rendered with key={index}, then the keys describe positions rather than items, and positions are exactly what a deletion changes. Delete the second of four rows and the third item now arrives with key 1, the fourth with key 2. React looks at key 1, finds the instance it already has there, and hands it the third item's props. Nothing unmounts, so the text the user had typed into the second row's input is still sitting in that instance, now displayed alongside the third item's data. From the user's side a value teleported into a different record, which is exactly the sort of bug that gets reported as data corruption.
// Index keys describe slots, so deleting a row shifts every key after it
// and surviving instances keep the state of the position they moved into.
{rows.map((row, i) => <Row key={i} row={row} />)}
// A key derived from the row's own identity travels with the row.
{rows.map((row) => <Row key={row.id} row={row} />)}
With row.id as the key, the deleted row's key disappears from the new children entirely, so React unmounts that instance and its state goes with it, and every other row is matched to the same instance it had before. Reordering behaves the same way: React moves the instances rather than reassigning their contents.
This is why the usual advice, that index keys are fine for a static list, is precise rather than lazy. A list that is only ever appended to, never filtered, never sorted and never has an item removed does behave identically under index keys. The trouble is that "we will never filter this list" is a claim about the future, and the failure when it becomes false is silent, produces no warning in the console, and shows up as a data problem rather than as a rendering one.
Diagnosing it without guessing
The distinction to establish first is whether components are remounting or being updated in place, because that separates this failure from the several others that look like it. Put a log in a mount effect on the row component, with an empty dependency array, and interact with the list. If deleting one row logs a mount for the rows below it, keys are being invalidated too aggressively. If deleting a row logs nothing at all, instances are being reused across different data, which is this bug. React's developer tools help in the same way from the other direction: an update highlight on rows whose data did not change is the visible symptom of props being reassigned to a surviving instance.
It is worth checking for duplicate keys at the same time. Two siblings with the same key produce a console warning, and behaviourally React can only keep one of them, so updates to one item appear to be swallowed. That is a different fault with a similar-looking report, and the warning is the tell.
The two keys that look clever and are not
key={Math.random()} guarantees a fresh key on every render, so every row unmounts and remounts every time the parent renders. The list appears to work, because state is destroyed rather than misassigned, and it is a poor trade: you lose focus and in-progress input constantly, you throw away every DOM node on each render, and any transition or effect inside the row restarts. A key derived from the item's contents, such as a serialised copy of the object, has a milder version of the same problem, since editing a field changes the key and remounts the row the user is currently typing into.
The rule that covers both is that a key must be stable for the lifetime of the item and unique among its siblings. Server identifiers satisfy that. For rows that do not exist server-side yet, generate an identifier when the row is created in the client and keep it on the row object, so the key is a property of the item from the moment it appears rather than something computed at render time. Keys also need to be attached to the outermost element produced per item, which for a list of fragments means using the explicit Fragment form so the key has somewhere to go.
The same mechanism, used deliberately
Once you see that a changed key means a fresh instance with fresh state, you have a tool as well as a hazard. A detail form that should forget everything when the user selects a different record can be given the record's identifier as its key, and switching records discards the old instance and all its state without a line of reset logic. That is more robust than an effect that clears fields on change, because it cannot miss a field that somebody adds later.
Both uses come from the same sentence, and being able to say it is what separates a candidate who has memorised "do not use index as key" from one who understands the model: keys are how you tell React which instance belongs to which item, and everything an instance remembers rides on that answer.
Component state is attached to a keyed slot in the tree, not to your data. Choose a key that identifies the item and the state follows the item; choose the array index and the state stays behind at the position while your data moves past it.
Likely follow-ups
- Rows have no server identifier until they are saved. Where does the key come from before that?
- Two rows end up sharing the same key. What does React do, and what does the user see?
- You want a form to reset when the selected record changes. Why is a key better than an effect that clears the fields?
- The list is virtualised so only twenty rows are mounted at once. Which of these problems does that change?
Related questions
- Your nostro reconciliation shows a break this morning. How do you investigate it, and how do you know when the money has actually gone?hardAlso on reconciliation6 min
- A client says your position does not match their custodian statement. How do you work out who is right, and what does a corporate action do to that answer?mediumAlso on reconciliation5 min
- A small percentage of your transaction reports are rejected every day and the team resubmits them the next morning. Is that acceptable?hardAlso on reconciliation6 min
- A customer's bank shows the money left their account and you have no order. How does your checkout make that impossible, and how do you find the ones that already happened?hardAlso on reconciliation5 min
- A fill arrives for an order your system has already marked cancelled. What do you do?hardAlso on reconciliation5 min
- The MES is unreachable for four hours and the line keeps producing. What happens to the production record, and how do you reconcile afterwards?hardAlso on reconciliation5 min
- A subscriber's service is working on the network but no subscription exists in billing. How did that happen, and what do you do about it?hardAlso on reconciliation5 min
- Finance and Operations quote different figures for the same monthly number and both insist they are right. How do you settle it?hardAlso on reconciliation4 min