React associates local state with a component's position, type, and key in the rendered tree. That means two visually similar panels can carry different drafts, and the same panel can keep a draft after its displayed record changes if its identity did not change. Keys are not a visual sorting aid. They tell React which child is which across renders. A record identifier is usually stable enough for a case row; an array index is not when rows are inserted, sorted, or filtered. Deliberately changing a key can reset a form, but doing that on every render destroys user input and focus. The design question is whose work the state belongs to and when that work should end.
React Component Identity and Keyed Draft Lifetime
Working case
A permit reviewer edits the private note for case 47 while the queue is sorted by age. A new urgent case enters at the top. The list uses positions as keys, so a draft note and its expanded state appear beside another case after reordering. Elsewhere, a detail panel is reused for case 62 without a new key, leaving the case 47 comment in the case 62 form. One defect preserves state for the wrong entity; the other loses association when positions move. The corrected queue uses case IDs as row keys. The editor subtree uses the selected case ID as its identity when switching cases should discard the previous local draft, while a separate saved-draft store retains any work that must survive navigation.
Implementation boundary
import { useState } from "react";
function PermitQueue({ cases, selectedCaseId }) {
const selectedCase = cases.find((permitCase) => permitCase.id === selectedCaseId);
return (
<section>
<ul>{cases.map((permitCase) => <li key={permitCase.id}>{permitCase.title}</li>)}</ul>
{selectedCase && <CaseNoteEditor key={selectedCase.id} permitCase={selectedCase} />}
</section>
);
}
function CaseNoteEditor({ permitCase }) {
const [note, setNote] = useState("");
return <label>Note for {permitCase.id}<textarea value={note} onChange={(event) => setNote(event.target.value)} /></label>;
}Write a state-lifetime table before changing keys: row selection, temporary input, persisted draft, request result, and focus. Keep each row keyed by an immutable server identifier, not by its current index, status, or randomly generated value. Put a keyed editor at the boundary where changing cases must create a fresh state instance. If switching away must preserve unsent text, save it under the case ID first and hydrate the new editor from that owned store; a key alone is not a persistence strategy. Avoid defining a component function inside another render function, because its function identity changes and can reset state unexpectedly. Test insertion, filtering, sort reversal, case switch, and browser back. In each test, assert both the visible record ID and the draft's owner, not merely that some text remains.
Cost and boundaries
Stable keys make reconciliation cheaper and avoid accidental remounts, but a deliberately keyed subtree remounts all descendants and can restart expensive widgets, focus, and effects. A saved-draft map adds memory proportional to active drafts and needs an expiry or submission cleanup rule. Keeping every hidden editor mounted to preserve state avoids remount cost but retains its resources and may run unnecessary effects. Choose the smallest subtree whose lifetime truly changes, then measure render work on a long queue. There is no useful universal key such as a timestamp: changing it every time makes every update a new component. A changing key should reflect a real ownership change.
Failure trace
Insert a case ahead of the currently edited row, reverse sorting, and filter out then restore that row. The draft must remain associated with its case or be intentionally stored elsewhere. Switch the detail panel from case 47 to case 62 with an unfinished note and verify the new form does not silently display the old case's text. Change an unrelated toolbar label; the editor must not reset. Remove a row while a request for it is pending and ensure its response cannot write into another row's state. Repeat with keyboard focus inside the editor, since an unexpected remount can erase the caret even when text is restored.
Verification
- Rows keep their draft owner after insert, sort, and filter.
- A deliberate case change resets only the intended editor subtree.
- An unrelated parent update does not erase text or focus.
Practice drill
Build a review queue containing cases 47, 62, and 81. Enter a note under case 47, then insert case 29 at the top, sort descending, and hide case 62. Record the case ID associated with every draft after each action. Next switch the detail editor to case 81 and decide whether the old draft should be persisted or discarded. State which subtree receives a case ID key and why. Add a test where a status badge changes without replacing the editor. Compare remount counts and focus behavior before and after the key change.
Decision note
A key expresses state ownership; use it only where the entity or intended lifetime changes.
Common Mistakes
- Using an array index as a key for a reorderable queue.
- Generating a fresh random key on every render.
- Assuming a key saves an unsent draft for later recovery.
Related lessons
React State and Rendering Boundaries; React Derived State and Event Ownership; React Urgent Input and Transition Work; React Hydration and Stable First Render; React Effects and Request Races; Autosave, Draft Recovery, and Conflicts.
Apply and check
Build Project: React permit review queue state and review Web Development: React state and rendering quiz.
