Identify essential state
Before adding a setter, identify the fact that needs memory between interactions. The record list and selected id are distinct facts. The selected title can be obtained from the list on each render and needs no separate state copy.
The same applies to a sum always derived from current prices. If the result is stored separately, every edit, removal and replacement must also update that copy. Structure should reduce these obligations.
Derived values can be local constants; optimizing their calculation is a later decision supported by measurement. Avoiding duplication does not forbid all local state: an editable draft can be a fact intentionally distinct from a confirmed value.
Valid states and explicit replacement
A process that can only be editing, saving or saved is clearer with one status than with independent booleans permitting contradictory combinations. Visual conditions can be derived from that status. If two coordinates always change together, an object can express the relationship, but useState does not automatically merge objects.
Replacing {x: 1, y: 2} with {x: 3} loses y from the new value. To preserve y, construct the next object with the required fields. Interface state does not certify an external result either: represent service confirmation only when evidence supports it. In this example, Save locally changes only the in-memory list.
Update the levels that change
The save handler uses map to construct a new array. For the changed record, it creates {...record, title}; for the others, it retains references because it changes none of their properties. Spreading an array does not copy its objects.
If you write next = [...records] and then next[0].title = value, the old object has also changed. For state with address.city, copy address and its containing object to replace city without writing into the previous snapshot. There is no need to indiscriminately clone every object in the application.
The objective is to preserve existing data and build the reference path corresponding to the change. This helps identify which component received new data and avoids affecting consumers of the earlier object.
Select by identity
Storing selectedItem as an object can duplicate information from the list. After replacing that item with an edited version, selection may still reference the old version. Storing selectedId and looking up the current object removes that duplication.
Also define what happens when lookup finds nothing: the list may be empty or the record may have been removed. The example displays No selected record rather than reading a property of undefined. Automatically choosing another record, clearing selection or retaining a removal notice are different policies.
Do not let a hidden assumption that find always succeeds determine product behavior.
Position, type and state continuity
React associates state with an instance in the rendered tree. Two Counter occurrences at different positions can have independent state despite using the same function. The same type at the same position normally retains state when props change; position in the JSX file is not the criterion.
If Editor is removed and replaced with a paragraph, that instance’s state is destroyed. Displaying Editor later creates a new instance. Retaining drafts for removed components would require another design, such as state owned by a component that remains mounted.
Analysis should follow the tree and ownership boundary rather than count JSX lines or rely on a function name.
Stable keys and deliberate resets
A list whose order can change needs item identity. An index represents current position and can attach local state to the wrong record after reordering. Use a stable id unique among siblings.
React interprets key; pass record or rowId separately if the component needs to read that data. A random key on each render forces new identity and can lose text and focus. In the example, Editor’s key is selected.id: switching records deliberately resets that entire subtree.
This is not a technique for synchronizing every field on every keystroke. Retain the key when the instance should continue and change it when policy requires starting again.
Initialization and draft policy
useState(record.title) uses the title to initialize the instance’s draft. New props for the same instance do not automatically reset that state. This can protect ongoing editing or cause divergence, depending on the contract.
The example uses initialRecords to make explicit that later props do not synchronize the local list. When id changes, reset is deliberate and loses the previous draft. A real application may require confirmation before switching; the excerpt does not implement that flow.
To assess a proposal, distinguish an ordinary update to the same record, identity change, removal from the tree and remote confirmation. Each requires a decision about retaining or replacing data.
import {useState} from 'react';
// Parent supplies unique, stable record IDs and string titles.
function Editor({record, onSave}) {
const [draft, setDraft] = useState(record.title);
return <section>
<label>Title <input value={draft}
onChange={e => setDraft(e.target.value)} /></label>
<button type="button" onClick={() => onSave(record.id, draft)}>Save locally</button>
</section>;
}
export default function RecordReview({initialRecords}) {
const [records, setRecords] = useState(initialRecords);
const [selectedId, setSelectedId] = useState(initialRecords[0]?.id ?? null);
const selected = records.find(record => record.id === selectedId);
function save(id, title) {
setRecords(current => current.map(record =>
record.id === id ? {...record, title} : record));
}
return <>
<ul>{records.map(record => <li key={record.id}>
<button type="button" onClick={() => setSelectedId(record.id)}>{record.title}</button>
</li>)}</ul>
{selected ? <Editor key={selected.id} record={selected} onSave={save} />
: <p>No selected record</p>}
</>;
}The example stores the list and selectedId, deriving the selected object. Saving creates a new object for the changed record. Changing id resets Editor: the contract discards the previous draft. initialRecords is initialization only; there is no synchronization with later props or remote persistence.
Common pitfalls
Copying only the array and mutating a shared object; duplicating the selected item; using indices in a reorderable list; generating keys during render; expecting useState to reinitialize whenever a prop changes.
Related topics: Events, state snapshots and update queues · Forms, DOM contracts and accessibility · Reducers, context and shared state
Keep one source for each fact and a stable identity for each instance that should continue. Copy changed levels and change keys only when resetting matches product policy.
Reference: Essential state and contradictions · React 19.3.0; JSX