React Input Loses Focus on Every Keystroke - The Remount Bug
7 min read
“You click into the field, type one character, and focus jumps out. Click back in, type another character, focus jumps out again. The input isn't losing focus - it's being destroyed and rebuilt, one keystroke at a time.”
Click into the input. Type "a". Focus disappears - the field is still there,
but the cursor is gone, and the next keystroke does nothing until you click
back in. Type "b". Focus disappears again.
Typing a full word is, practically, impossible.
This isn't a focus bug in the way it feels. Nothing is "stealing" focus.
The input element you clicked into is being thrown away and replaced with a
brand new one - and the new one, understandably, isn't focused.
On every render, React compares what it rendered last time to what it's
rendering now, element by element. For each element, it asks: is this the
same type as last time, in the same position?
If yes, React updates the existing instance in place - same DOM node, same
internal state, same focus, just with new prop values applied. If no, React
unmounts the old instance entirely - destroying its DOM node and any state
it held - and mounts a brand new one from scratch.
A focused <input> that gets unmounted and remounted is, simply, no longer
focused. The new <input> is a different DOM node. Browsers don't transfer
focus between DOM nodes for you.
NameField is defined insideSignupForm's function body. Every time
SignupForm renders, this line runs again - and const NameField = (...) => ... creates a brand new function, at a brand new memory address, every
single time.
Type one character. setName updates state. SignupForm re-renders. The
line defining NameField runs again, producing function #2. React compares
function #1 (last render's type for this element) against function #2 (this
render's type). They are different functions - different references -
even though they're defined identically. React treats this as "a different
component type," unmounts the <NameField> from function #1 (destroying its
<input> and any focus on it), and mounts a fresh one from function #2.
Symptom
Type one character, focus is gone, the value did update (you can see your
character in the field after clicking back in) - but the cursor isn't
there anymore. Every keystroke requires a fresh click into the field.
NameField now exists once, at module scope, created when the file loads.
Every render of SignupForm refers to the exact same function reference.
React's type comparison passes - same type as last time - so it updates the
existing <NameField> and its <input> in place. Same DOM node. Same
focus. Typing works.
The Sensei's Hint
This is specifically about components defined inside another component's
body - functions, arrow functions, anything that creates a new component
type on every render. A component defined at module scope, or imported
from another file, doesn't have this problem no matter how many times the
parent re-renders.
React's "same type, same position" rule has one more input: the key prop.
Two elements of the same type with different keys are treated as different
components - intentionally. This is usually exactly what you want, except
when the key changes for reasons you didn't intend.
Log a few sessions - the counter goes up correctly. Now type one character
into the filter input. The counter drops back to 0.
key={\progress-$`}tiesProgressCounter's identity to the filter text. Every keystroke changes subject, which changes the key, which tells React "this is a different ProgressCounternow" - so it unmounts the old one (with itssessions` state) and mounts a fresh one starting at 0.
<ProgressCounter key="progress" />
A stable key - one that never changes - means React always sees "the same
ProgressCounter" across renders, regardless of what subject does. The
existing instance updates in place, and sessions survives.
The Sensei's Hint
A changing key isn't always a bug - sometimes it's the fix. If a form's
fields should fully reset whenever a different record is loaded,
key={record.id} on the form intentionally forces a clean remount with
fresh initial state for each record. The difference is whether the reset
is something you want.
This one is the opposite problem. Click a different user button - the
input keeps showing the first user's name. useState(user.name) only runs
its initializer on the component's first mount; every render after that,
React reuses the existing ProfileForm instance and its existing name
state, completely ignoring that user is now a different object.
<ProfileForm key={user.id} user={user} />
Adding key={user.id} makes the fix: when user.id changes, React now
treats this as a differentProfileForm - unmounts the old one, mounts a
new one, and useState(user.name) runs its initializer fresh, with the new
user's name.
Cases one and three are about a key (or component type) changing when it
shouldn't - causing unwanted resets. Case two's fix is the inverse: a key
that was changing when it shouldn't, made stable. Same underlying rule -
"does React see the same component identity as last render?" - producing
opposite-looking bugs depending on which direction the identity is unstable.
Constraints
Component identity comes from two things together: the component's type
(the function itself - watch out for functions defined inside other
components) and its key (if one is given - watch out for keys built from
values that change for unrelated reasons). If either one differs from the
previous render, React remounts. State, DOM nodes, and focus do not survive
a remount.
"Can't Type Without Losing Focus" is the inline-component version - the
purest form of this bug, and the one you'll recognize fastest in real
codebases. "Form Keeps Showing the Previous User" flips it around: here, a
missing key is the bug, and adding one is the fix - useful for seeing both
directions of the same mechanism. "Counter Resets While Typing" closes the
loop with an unstable key causing unwanted remounts on every keystroke -
work through all three to get comfortable with "what makes React think this
is a new component?" from every angle.