The Error
You open the browser console and there it is:
Warning: A component is changing an uncontrolled input of type 'text' to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
The form still works โ sort of. But this warning signals that your input is switching identity mid-render. That flip causes real bugs: stale values, broken validation, the cursor jumping to the end while you type. Don't ignore it.
Why This Happens
React draws a hard line between two types of inputs:
- Uncontrolled: no
valueprop โ the DOM owns the state. - Controlled:
valueis tied to React state โ React is the source of truth.
The warning fires when an input starts life as uncontrolled (value={undefined}), then flips to controlled (value="something") after a state update. React cannot handle that mid-lifecycle identity crisis.
Nine times out of ten, the culprit is dead simple: state initialized as undefined or null instead of an empty string.
Step-by-Step Fix
Step 1 โ Track down the broken input
The warning usually names the component. If it doesn't, open React DevTools and hunt for inputs where value toggles between undefined and a real string across renders. Start with your state initialization โ that's where the problem lives 90% of the time.
Step 2 โ Initialize state with an empty string
This one change fixes the vast majority of cases. Here's the broken pattern:
// BROKEN โ value starts as undefined
const [name, setName] = useState();
const [email, setEmail] = useState(null);
return (
<form>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
And here's the fix:
// FIXED โ value starts as empty string
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
</form>
);
Step 3 โ Fix object-based form state
Many forms store all fields in a single state object. When you do that, every field needs an explicit string default โ missing fields silently become undefined:
// BROKEN โ form.email is undefined โ uncontrolled on first render
const [form, setForm] = useState({ name: '' });
// FIXED โ every field declared upfront
const [form, setForm] = useState({
name: '',
email: '',
phone: '',
message: ''
});
const handleChange = (e) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
Step 4 โ Defend against API data
Fetch user data to pre-fill a form and you hit a timing problem. While the request is in flight, user is null โ which means every input value is undefined on the first render:
// BROKEN โ user?.name is undefined while fetching
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(data => setUser(data));
}, []);
return (
<input type="text" value={user?.name} onChange={...} />
);
// FIXED โ nullish coalescing as a safety net
return (
<input type="text" value={user?.name ?? ''} onChange={...} />
);
Better yet, initialize user with the full shape so there's nothing to fall back from:
const [user, setUser] = useState({ name: '', email: '', role: '' });
useEffect(() => {
fetchUser().then(data => setUser(data));
}, []);
Step 5 โ Don't mix value and defaultValue
For an uncontrolled input that needs a starting value, use defaultValue. Using both value and defaultValue on the same input is a frequent mistake:
// Mixing controlled and uncontrolled โ pick one
<input value={name} defaultValue="John" onChange={...} />
// Controlled โ state drives everything
<input type="text" value={name} onChange={e => setName(e.target.value)} />
// Uncontrolled โ DOM manages value, read it via ref
<input type="text" defaultValue="John" ref={inputRef} />
Verify the Fix
- Open Chrome DevTools โ Console tab.
- Reload and interact with the form: type, submit, clear a field.
- The warning
A component is changing an uncontrolled inputshould be gone. - In React DevTools, check the component โ the input's
valueprop should always be a string. Neverundefined, nevernull.
Warning still showing? Cast a wider net. Search the entire component tree for <input>, <textarea>, and <select> elements โ any of them can trigger it.
Quick Checklist
useState()with no argument โ change touseState('')useState(null)for a text field โ change touseState('')- Object state with missing fields โ declare all fields with
''defaults - API data used directly as
valueโ add?? ''fallback - Both
valueanddefaultValueon the same input โ remove one - Checkbox or radio? Use
checkedwith afalsedefault, notundefined
One More Edge Case: Checkboxes
The same warning hits checkboxes when checked starts as undefined. Same fix, different default value:
// BROKEN
const [agreed, setAgreed] = useState(); // undefined
<input type="checkbox" checked={agreed} onChange={e => setAgreed(e.target.checked)} />
// FIXED
const [agreed, setAgreed] = useState(false);
<input type="checkbox" checked={agreed} onChange={e => setAgreed(e.target.checked)} />
The underlying principle: once you put a value or checked prop on an input, React owns it. That means React's state must be a valid value from the very first render โ an empty string for text fields, false for checkboxes. undefined and null hand control back to the DOM, which is exactly what starts the fight.

