“You have a contact list and an edit form beside it. You type a change for Alice, then click Bob, and the form still shows Alice’s half-edited text. How do you fix it?” Most candidates reach for a useEffect that copies the new contact into the form state. The interviewer is usually hoping for a one-word answer: key.
This question checks whether you understand how React decides that a component is “the same one” between renders, which is the rule behind both this bug and its fix.
Before you start
You should know useState, how controlled inputs work, and what a key does in a list (see list keys and identity). The rules described here are identical in React 18 and 19.
The short answer
React preserves a component’s state as long as the same component type renders at the same position in the tree. Switching from Alice to Bob changes props but not position, so the form’s local draft state survives. Giving the form key={contact.id} tells React that a different key means a different component: it unmounts the old form, discards its state, and mounts a fresh one initialised from Bob’s data. This is simpler and more correct than an effect that resets state after the fact.
How it works
During reconciliation, React compares the new element tree with the previous one slot by slot. At each slot it asks two questions: is the type the same, and is the key the same? If both match, it updates the existing component and keeps its state and DOM. If either differs, it destroys the old one and creates a new one.
function ContactManager({ contacts }) {
const [selectedId, setSelectedId] = useState(contacts[0].id);
const selected = contacts.find(c => c.id === selectedId);
return (
<>
<ContactList contacts={contacts} onSelect={setSelectedId} />
<EditContact key={selected.id} contact={selected} />
</>
);
}
function EditContact({ contact }) {
const [name, setName] = useState(contact.name);
const [email, setEmail] = useState(contact.email);
return (
<form>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
}useState(contact.name) only reads its argument on mount. Without the key, the form mounts once with Alice and never reads Bob’s name. With the key, each selection is a new mount, so the initial value is read again.
Step-by-step walkthrough
Step 1: Reproduce the stale form
Remove the key and select Alice, type “Alicia”, then select Bob. The inputs still say “Alicia” and Alice’s email. React saw <EditContact> at the same position both times and kept its state.
Step 2: Notice why the effect fix is worse
The tempting fix:
useEffect(() => {
setName(contact.name);
setEmail(contact.email);
}, [contact]);It works, but the first render after switching shows Alice’s values with Bob’s props, the effect then corrects it, and a second render follows. Every new piece of state in the form needs another line in this effect, and forgetting one leaks data between records. If contact is a new object on each parent render, the effect also wipes the user’s typing on unrelated re-renders.
Step 3: Add a key that represents identity
key={selected.id} is the identity of the thing being edited. Use a stable id, not an index or a random value. A random key (key={Math.random()}) remounts on every render and destroys typing instantly.
Step 4: Choose the smallest subtree to reset
Put the key on the component whose state should reset, not on a page-level wrapper. Keying a whole page remounts every child, re-runs every effect and refetches data. If only part of a component should reset, extract that part into a child component and key the child.
Worked scenario
A support tool shows a ticket and a reply box. Agents complained that a reply typed for one ticket appeared under the next ticket after clicking “Next”. Worse, the attachment picker kept the previous ticket’s files, and one agent sent a customer’s invoice to a different customer.
The component tree looked like this:
<TicketView ticket={ticket}>
<ReplyBox ticketId={ticket.id} />
</TicketView>ReplyBox held text, attachments and cc in local state. A previous fix had added an effect to reset text, but nobody remembered the attachments.
The real fix:
<ReplyBox key={ticket.id} ticketId={ticket.id} />All three pieces of state, plus any added later, reset automatically, because the reply box is now tied to the ticket’s identity. The effect was deleted.
If the team later wants drafts to survive navigation, the state must move out of the keyed component, for example into a map keyed by ticket id in the parent, because a remount always discards local state.
Common mistake
- “Keys are only for lists.” Any element can take a key, and outside lists it is the standard way to request a reset.
- Resetting with an effect. It causes an extra render with mismatched data and has to be kept in sync with every state variable.
- Unstable keys. Indexes or random values either fail to reset or reset constantly.
- Keying too high. Remounting a large tree throws away scroll positions, focus and fetched data you wanted to keep.
- Expecting a key to preserve state across positions. Moving a component to a different parent changes its position, so state is lost regardless of key.
Verify the behavior
Add a mount log and switch records:
function EditContact({ contact }) {
const [name, setName] = useState(contact.name);
useEffect(() => {
console.log('mounted for', contact.id);
return () => console.log('unmounted for', contact.id);
}, []);
return <input value={name} onChange={e => setName(e.target.value)} />;
}With key={selected.id}, switching from contact 1 to contact 2 logs unmounted for 1 then mounted for 2, and the input shows the new name. Without the key, nothing logs and the input keeps the old text. (Strict Mode adds an extra mount and unmount pair on first load in development.)
Follow-up questions
Is there a way to adjust state on prop change without remounting? Yes: compare the prop with a stored previous value during render and set state immediately. React re-renders before committing, so no stale frame appears. It is rarely needed; a key or deriving the value is usually clearer.
Does a key reset DOM state too? Yes. A remount creates new DOM nodes, so uncontrolled inputs, focus and scroll position inside the subtree are reset as well.
Does changing the key re-run effects? Yes. The old instance’s cleanups run and the new instance’s effects run, as with any unmount and mount.
What if two different components render at the same position? Different types always reset, even without keys; {isAdmin ? <AdminForm /> : <UserForm />} never shares state.
Interview exercise
A chat app renders <Chat contact={to} />. When the user switches contacts, the message input keeps the half-written text for the previous person. Give two fixes with different behaviours and say when you would use each.
Answer and reasoning
Fix one: <Chat key={to.id} contact={to} />. Switching contacts remounts Chat, so the draft resets to empty. Use this when drafts should not survive navigation. Fix two: lift drafts into the parent as an object keyed by contact id, such as drafts[to.id], and pass the draft and an onDraftChange handler down. The input now shows each contact’s own draft when you return to them. Use this when preserving work matters. A good answer explains that the original bug happens because Chat stays at the same position with the same type, so React preserves its state across prop changes.
Continue learning
Practise with the React interview questions and the React MCQs. Related notes: list keys and identity, reconciliation and keys and controlled and uncontrolled inputs. Official reference: Preserving and Resetting State on react.dev.