This warning occurs when an input element switches from being controlled by React state to being uncontrolled, typically when the value prop changes from a defined value to undefined or null. The fix involves ensuring consistent state initialization and preventing undefined values from being passed to controlled inputs.
This warning appears when React detects that an input element has switched from being controlled (where its value is managed by React state) to being uncontrolled (where the DOM manages its own state). React requires that an input element remain either controlled or uncontrolled throughout its entire lifetime. A controlled input is one where you pass a value prop that is bound to React state, and typically also provide an onChange handler to update that state. When the value prop transitions from a defined value (like a string) to undefined or null, React interprets this as the input becoming uncontrolled, which triggers this warning. This issue most commonly occurs when state is initialized as undefined, when data is being loaded asynchronously and hasn't arrived yet, or when optional form fields conditionally receive undefined values. React is strict about this because switching between paradigms can lead to unpredictable behavior and loss of user input.
The most common fix is to ensure your state is initialized with an empty string, not undefined:
// ❌ Bad - starts as undefined
const [name, setName] = useState();
// ❌ Also bad - explicitly undefined
const [email, setEmail] = useState(undefined);
// ✅ Good - starts as empty string
const [name, setName] = useState('');
// ✅ Also good - explicit empty string
const [email, setEmail] = useState<string>('');This ensures the input is controlled from the very first render, preventing the controlled-to-uncontrolled switch.
When your value might be undefined (from props or API data), provide a fallback:
// ❌ Bad - value can be undefined
<input value={userData?.name} onChange={handleChange} />
// ✅ Good - always provides a string
<input value={userData?.name ?? ''} onChange={handleChange} />
// Also works with logical OR
<input value={userData?.name || ''} onChange={handleChange} />The nullish coalescing operator (??) or logical OR (||) ensures the value prop never receives undefined.
When loading data from an API, initialize your form state before data arrives:
function UserForm() {
// ❌ Bad - starts undefined until data loads
const [name, setName] = useState(userData?.name);
// ✅ Good - starts as empty, updates when data arrives
const [name, setName] = useState('');
useEffect(() => {
if (userData?.name) {
setName(userData.name);
}
}, [userData]);
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}This pattern ensures the input remains controlled even before data loads.
If you must work with potentially undefined values, sanitize them in your handler:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const newValue = e.target.value;
// Ensure we never set undefined
setName(newValue ?? '');
}
// Or more robustly
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setName(e.target.value || '');
}This defensive approach prevents undefined from ever entering your state.
If you don't need to control the input with React state, make it fully uncontrolled:
// ❌ Bad - mixing controlled and uncontrolled
<input value={undefined} />
// ✅ Good - fully uncontrolled with defaultValue
<input defaultValue="" ref={inputRef} />
// Access value via ref when needed
function handleSubmit() {
const value = inputRef.current?.value;
}Use defaultValue instead of value, and access the current value via a ref. This is appropriate for simple forms where you don't need real-time validation.
When rendering form fields from an array, ensure each has a valid value:
// ❌ Bad - some items might not have the property
{fields.map(field => (
<input
key={field.id}
value={field.value}
onChange={(e) => handleFieldChange(field.id, e.target.value)}
/>
))}
// ✅ Good - guarantee every input has a string value
{fields.map(field => (
<input
key={field.id}
value={field.value ?? ''}
onChange={(e) => handleFieldChange(field.id, e.target.value)}
/>
))}This pattern is especially important when working with dynamic forms or field arrays.
Ensure your form reset or clear functions don't introduce undefined values:
// ❌ Bad - resets to undefined
function resetForm() {
setName(undefined);
setEmail(undefined);
}
// ✅ Good - resets to empty strings
function resetForm() {
setName('');
setEmail('');
}
// Or use an initial state constant
const INITIAL_STATE = { name: '', email: '' };
function resetForm() {
setFormData(INITIAL_STATE);
}Always reset controlled inputs to empty strings, not undefined.
TypeScript users: Enable strict null checks in your tsconfig.json to catch undefined values at compile time. Type your state explicitly as useState<string>('') rather than relying on inference.
Performance consideration: If you have many form fields and are concerned about re-renders, consider using uncontrolled components with refs and accessing values only on submit. This trades off real-time validation for better performance.
React 19 changes: React 19 has improved handling of this scenario and provides clearer error messages. However, the fundamental rule remains: an input must be consistently controlled or uncontrolled throughout its lifetime.
Form libraries: Libraries like React Hook Form and Formik handle this complexity for you by ensuring all registered fields maintain controlled state consistently. Consider using them for complex forms.
Select and textarea elements: This same warning applies to <select> and <textarea> elements. The fixes are identical - ensure their value props never transition from defined to undefined.
Testing: When writing tests, ensure you initialize component state properly. Test utilities like React Testing Library's userEvent will respect controlled input behavior.
Common Redux pattern: When using Redux or other state management, ensure your selectors return empty strings for missing values rather than undefined: const name = useSelector(state => state.form.name ?? '');
React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite