The React useState hook is trying to manage the state of an input element, but it’s getting confused because the input’s value prop is sometimes controlled by React and sometimes not.
Here’s what’s actually breaking: React’s reconciliation process expects a component’s state to be the single source of truth for its UI. When an input element has a value prop, React assumes it’s controlling that input. If you then let the user type into the input without updating React’s state accordingly, React sees a mismatch: the DOM element’s value has changed, but its own internal state hasn’t. This leads to the warning.
Common Causes and Fixes:
-
Initial State Mismatch & User Input Before State Update
- Diagnosis: This happens when you initialize your input’s state with an empty string or
null, but the input element itself might have adefaultValueattribute set in your JSX. The user types, the DOM updates, but React’s state is stillnullor"". - Check: In your component, look for
<input value={stateVariable} ... />and see whatstateVariableis initialized to. Also, check if the input has adefaultValueattribute. - Fix: Ensure your initial state matches what the input might render initially, or remove
defaultValueif you intend to control it from the start.// If you want React to control it from the very beginning const [inputValue, setInputValue] = useState(''); // Initialize with the expected type of value // ... <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> - Why it works: By initializing
useStatewith a value that matches the input’s expected type (e.g., an empty string for text inputs), you satisfy React’s expectation that its state is the source of truth from the outset.
- Diagnosis: This happens when you initialize your input’s state with an empty string or
-
Asynchronous State Updates Causing Stale Props
- Diagnosis: You might be fetching data and setting the input’s state based on that data, but the component re-renders before the state update from the fetch has completed. The initial render uses a default/empty state, and a subsequent render uses the fetched data, but an intermediate render might have seen a controlled-then-uncontrolled state.
- Check: Look for
useEffecthooks that fetch data and then callsetInputValue. Inspect the render cycle. - Fix: Ensure that you only render the controlled input after the initial data is loaded. A common pattern is to conditionally render the input or provide a default value until the asynchronous operation finishes.
const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(true); useEffect(() => { fetch('/api/initial-value') .then(res => res.json()) .then(data => { setInputValue(data.value); setIsLoading(false); }); }, []); if (isLoading) { return <div>Loading...</div>; } return <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />; - Why it works: This prevents the component from trying to render a controlled input before it has a valid controlled value, avoiding the transient uncontrolled state.
-
Conditional Rendering of the
valueProp- Diagnosis: You might have logic that sometimes provides a
valueprop to the input and sometimes doesn’t, based on other component state. - Check: Examine any ternary operators or
ifstatements that decide whether to passvalue={stateVariable}to the input. - Fix: Always provide the
valueprop, even if it’s an empty string, when you intend to control the input.const [isEditing, setIsEditing] = useState(false); const [text, setText] = useState('initial text'); return ( <input value={isEditing ? text : ''} // Always provide a value, even if it's empty onChange={(e) => setText(e.target.value)} disabled={!isEditing} /> ); - Why it works: The input’s
valueprop is consistently managed by React’s state, regardless of theisEditingflag. WhenisEditingis false, it defaults to an empty string, which is still a controlled value.
- Diagnosis: You might have logic that sometimes provides a
-
Using
defaultValueandvalueSimultaneously- Diagnosis: You might have set
defaultValuein your JSX and then tried to control the input with thevalueprop. React seesdefaultValueas an initial, uncontrolled value, but then expectsvalueto always be present. - Check: Look for
<input defaultValue="..." value={stateVariable} ... />in your JSX. - Fix: Remove
defaultValueif you intend to use thevalueprop for controlled behavior.// Remove defaultValue entirely const [text, setText] = useState('initial text'); return <input value={text} onChange={(e) => setText(e.target.value)} />; - Why it works:
defaultValueis a one-time initialization. If you want React to manage the input’s value throughout its lifecycle, you must rely solely on thevalueprop andonChangehandler.
- Diagnosis: You might have set
-
Server-Side Rendering (SSR) Mismatch
- Diagnosis: If you’re using SSR, the initial HTML rendered on the server might have a value, but the client-side React hydration process expects the state to match that value. If your client-side state is initialized differently (e.g., empty), this warning appears.
- Check: Are you using SSR? Does the initial value rendered by the server match the initial state of your React component on the client?
- Fix: Ensure that the initial state in your client-side component matches the
valueattribute that was rendered on the server.// On the server, render <input value="server-rendered-value" /> // In your React component: const [inputValue, setInputValue] = useState('server-rendered-value'); // Initialize with the server's value // ... rest of your controlled input logic - Why it works: Hydration requires the client-rendered tree to match the server-rendered tree. By initializing the client-side state with the same value that was present in the server-rendered HTML, you ensure a consistent starting point.
-
refandvalueConflict- Diagnosis: You might be using a
refto directly access and manipulate the DOMvalueof an input, bypassing React’s state management. - Check: Look for
useRefandref.current.value = ...assignments that modify the input’s value directly. - Fix: Avoid directly manipulating
ref.current.valuefor controlled inputs. Instead, usesetStateto update the value.const inputRef = useRef(null); const [inputValue, setInputValue] = useState(''); const handleChange = (e) => { setInputValue(e.target.value); // DON'T do this: inputRef.current.value = e.target.value.toUpperCase(); // Instead, update state and let React re-render }; return <input ref={inputRef} value={inputValue} onChange={handleChange} />; - Why it works: This reinforces React’s declarative model. State changes trigger re-renders, and the
valueprop ensures the input reflects the current state, rather than imperative DOM manipulation causing divergence.
- Diagnosis: You might be using a
After fixing these, the next thing you’ll likely encounter is a warning about an input being uncontrolled because it has an onChange handler but no value prop, or vice versa.