This warning occurs when an input element switches from being uncontrolled (managed by the DOM) to being controlled by React state, typically when the value prop changes from undefined to a defined value. 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 uncontrolled (where the DOM manages its own state) to being controlled (where its value is managed by React state). React requires that an input element remain either controlled or uncontrolled throughout its entire lifetime. An uncontrolled input is one where you don't provide a value prop (or provide undefined/null), allowing the DOM to manage its own state. When the value prop transitions from undefined/null to a defined value (like a string), React interprets this as the input becoming controlled, which triggers this warning. This issue most commonly occurs when state is initialized as undefined, when data is being loaded asynchronously and arrives later, or when form fields conditionally receive 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 uncontrolled-to-controlled switch.
When your value might be undefined (from props or API data), provide a fallback:
// ❌ Bad - value can be undefined initially
<input value={userData?.name} onChange={handleChange} />
// ✅ Good - always provides a string from the start
<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, even during loading states.
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 don't need to control the input with React state, make it fully uncontrolled:
// ❌ Bad - mixing uncontrolled and controlled
<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 or state synchronization.
When rendering form fields from an array, ensure each has a valid value from the start:
// ❌ Bad - some items might not have the property initially
{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 from the start
{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 that load data asynchronously.
For checkboxes and radio buttons, ensure the checked prop is always boolean:
// ❌ Bad - checked can be undefined
const [isChecked, setIsChecked] = useState();
return (
<input
type="checkbox"
checked={isChecked}
onChange={(e) => setIsChecked(e.target.checked)}
/>
);
// ✅ Good - always boolean
const [isChecked, setIsChecked] = useState(false);
return (
<input
type="checkbox"
checked={isChecked}
onChange={(e) => setIsChecked(e.target.checked)}
/>
);
// For radio buttons with potentially undefined value
<input
type="radio"
checked={selectedValue === (optionValue ?? '')}
onChange={() => setSelectedValue(optionValue ?? '')}
/>Always initialize boolean state with false (or true) rather than undefined.
Ensure conditional rendering doesn't introduce undefined values:
// ❌ Bad - renders with undefined value initially
{isLoading ? (
<Spinner />
) : (
<input value={data.name} onChange={handleChange} />
)}
// ✅ Good - always provides a value
{isLoading ? (
<input value="" onChange={handleChange} disabled />
) : (
<input value={data.name} onChange={handleChange} />
)}
// Or use a loading state with fallback
const inputValue = isLoading ? '' : data.name;
return <input value={inputValue} onChange={handleChange} />;If you need to hide inputs during loading, consider disabling them with empty values instead of removing them.
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.
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 consistent state. 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 undefined to defined.
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.
Testing: When writing tests, ensure you initialize component state properly. Test utilities like React Testing Library's userEvent will respect controlled input behavior.
Common pattern with Redux: 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 ?? '');
Key prop for truly dynamic behavior: In rare cases where you genuinely need to switch an input from uncontrolled to controlled (e.g., taking over control after user action), use a different key prop to force React to create a new instance: <input key={isControlled ? "controlled" : "uncontrolled"} ... />
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