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:

  1. 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 a defaultValue attribute set in your JSX. The user types, the DOM updates, but React’s state is still null or "".
    • Check: In your component, look for <input value={stateVariable} ... /> and see what stateVariable is initialized to. Also, check if the input has a defaultValue attribute.
    • Fix: Ensure your initial state matches what the input might render initially, or remove defaultValue if 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 useState with 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.
  2. 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 useEffect hooks that fetch data and then call setInputValue. 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.
  3. Conditional Rendering of the value Prop

    • Diagnosis: You might have logic that sometimes provides a value prop to the input and sometimes doesn’t, based on other component state.
    • Check: Examine any ternary operators or if statements that decide whether to pass value={stateVariable} to the input.
    • Fix: Always provide the value prop, 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 value prop is consistently managed by React’s state, regardless of the isEditing flag. When isEditing is false, it defaults to an empty string, which is still a controlled value.
  4. Using defaultValue and value Simultaneously

    • Diagnosis: You might have set defaultValue in your JSX and then tried to control the input with the value prop. React sees defaultValue as an initial, uncontrolled value, but then expects value to always be present.
    • Check: Look for <input defaultValue="..." value={stateVariable} ... /> in your JSX.
    • Fix: Remove defaultValue if you intend to use the value prop 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: defaultValue is a one-time initialization. If you want React to manage the input’s value throughout its lifecycle, you must rely solely on the value prop and onChange handler.
  5. 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 value attribute 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.
  6. ref and value Conflict

    • Diagnosis: You might be using a ref to directly access and manipulate the DOM value of an input, bypassing React’s state management.
    • Check: Look for useRef and ref.current.value = ... assignments that modify the input’s value directly.
    • Fix: Avoid directly manipulating ref.current.value for controlled inputs. Instead, use setState to 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 value prop ensures the input reflects the current state, rather than imperative DOM manipulation causing divergence.

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.

Want structured learning?

Take the full React course →