React logs "A component is changing an uncontrolled input to be controlled". What causes that warning in a React form, and how do you fix it?
answer
- one prop went from undefined to a string
- React decides at mount which kind it is
- null counts as not supplied
- empty-string initial state, or ?? ''
- checkboxes hit it through checked
basics
~20 sThe input's value prop started as undefined or null, so React treated the field as uncontrolled, and a later render passed a string, flipping it to controlled. Fix it by initialising the state to an empty string or writing value={x ?? ''}.
solid answer
~50 sReact decides on the first render whether an input is controlled by looking at whether `value` is defined. `undefined` or `null` means uncontrolled, so the DOM owns the text; once a later render supplies a string, React takes ownership and warns that the input switched sides. The usual causes are `useState()` with no initial argument, form state loaded asynchronously so `value={user.name}` is `undefined` on the first pass, or a key that simply is not present in a defaults object. The fix is to make the value a string from the very first render: `useState('')`, or `value={user?.name ?? ''}` at the call site. For checkboxes the same applies to `checked`, so use `checked={!!x}`. If the data genuinely is not there yet, either do not render the form until it loads, or render it uncontrolled with `defaultValue` and remount it with a `key` once the record arrives. The mirror-image warning — controlled becoming uncontrolled — has the same cause with the values reversed.
code
jsx · 18 linesimport { useState } from 'react';
// Warns: value is undefined on the first render, a string afterwards.
export function Broken({ user }) {
const [name, setName] = useState();
return <input value={name ?? user?.name} onChange={(e) => setName(e.target.value)} />;
}
// Fixed: the value prop is a string from render one.
export function Fixed({ user }) {
const [name, setName] = useState('');
return (
<input
value={name || user?.name || ''}
onChange={(e) => setName(e.target.value)}
/>
);
}go deeper
Recall that a controlled input's value must never be undefined or null, and that initialising state with an empty string is the everyday fix.
Explain that React locks in the controlled decision at mount from whether value is defined, name the async-data and empty-useState causes, and show the ?? '' coercion.
Point out the real damage — typed text wiped when ownership flips — and choose deliberately between coercing the value, gating the render on loaded data, and remounting the form with a key.
Own the boundary policy: normalise nullable server fields into form-safe strings once, at the data layer, so no component has to remember the coercion and the warning cannot reappear across teams.
## What React is actually checking When an input mounts, React records whether it is controlled by testing the `value` prop (or `checked` for checkboxes and radios). `undefined` and `null` both count as "not supplied", so the input is registered as uncontrolled and the DOM node keeps the text. On every later render React compares that decision against what it now sees. If the prop has become a string, the input has changed sides, and React logs: > A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. It is a development-only warning, fired once per component, but it points at a real bug: for the lifetime of that field, ownership of its value moved. ## The three ways it happens **1. A state hook with no initial argument.** ```jsx const [query, setQuery] = useState(); // undefined on the first render <input value={query} onChange={(e) => setQuery(e.target.value)} /> ``` The first render is uncontrolled; the first keystroke sets a string and the field becomes controlled. **2. Data that arrives later.** ```jsx function ProfileForm({ user }) { // user is undefined while loading return <input value={user?.name} onChange={...} />; } ``` The form renders before the fetch resolves. `user?.name` is `undefined`, so the field is uncontrolled; when the response lands it becomes a string. **3. A missing key in an object of form values.** A field named `phone` reads `values.phone`, but the initial object was built without that key. Same shape, harder to spot, because the object itself is defined. ## Why it is more than noise While the field is uncontrolled, anything the user types lives only in the DOM. The moment React takes control, it writes the state value into the node and whatever was typed is gone. In the async case this is a real data-loss bug: a fast typist fills in a field and the arriving response silently wipes it. Reactively-derived UI is also inconsistent in the meantime — validation and submit buttons read state that does not match the screen. ## The fixes, in order of preference **Give the value a string from render one.** Never let it be `undefined`: ```jsx const [query, setQuery] = useState(''); // or, when the value comes from props/context: <input value={user?.name ?? ''} onChange={...} /> ``` Use `??` rather than `||` when an intentional empty-ish value matters — for a number field, `0 || ''` collapses a legitimate zero to an empty string, while `0 ?? ''` keeps the zero. **For checkboxes, coerce to boolean:** `checked={!!settings.optIn}`. `undefined` there produces the same warning against `checked`. **Do not render the form before the data exists.** Returning a skeleton or a spinner until the record loads sidesteps the whole problem, and it is often the honest answer because a form pre-populated with blanks is not a good empty state anyway. **Or stay uncontrolled and remount.** Render the fields with `defaultValue` and give the form a `key` derived from the record id. When the id changes, React discards the old fields and mounts new ones seeded with the fresh defaults — no ownership switch at all. What does *not* work is passing the loaded value to `useState` and expecting it to catch up: the initial argument is only read on the first render, so `useState(user?.name ?? '')` bakes in the empty string and never updates when `user` arrives. That is the trap immediately after the naive fix, and it is why the remount-with-key or don't-render-yet options exist. ## The mirror-image warning "A component is changing a controlled input to be uncontrolled" is the same mechanism reversed: the value was a string and became `undefined` or `null`. Typical causes are a reset that does `setValues({})` or `setValue(null)` instead of `setValue('')`, or a server response whose field is JSON `null`. The rule that prevents both is the same: for a controlled field, the value prop is always a string (or always a boolean for `checked`), from the first render to the last. ## How to answer this in an interview State the mechanism first — React fixes the controlled/uncontrolled decision at mount from whether `value` is defined — then name `undefined` from an unset state hook or unloaded data as the cause, then give the `?? ''` fix and mention that the mirror warning has the same root. Adding "and the user's typing gets wiped when ownership flips" shows you understand why it matters rather than just how to silence it.
- Why does moving the loaded value into useState's initial argument not fix the async case?Because the initial argument is only evaluated on the first render. `useState(user?.name ?? '')` captures the empty string while the fetch is in flight and ignores the value that arrives later — state is not re-initialised. Either delay rendering the form until the record exists, or mount fresh fields by giving the form a `key` tied to the record id.
- What does the mirror warning, "changing a controlled input to be uncontrolled", usually mean?That a value that was a string became `undefined` or `null`. Common sources are a reset written as `setValues({})`, deleting a key from the values object, or a server field that is JSON `null` being passed straight to `value`. The same rule fixes it: keep the prop a string always, coercing with `?? ''` at the boundary.
- Does the warning ever appear in production builds?No — it is a development-only warning stripped from production builds, and React logs it only once per component instance. That is precisely why it should not be ignored in development: production keeps the bug and loses the diagnostic.
saying these in an interview costs you the question
- Suggests silencing the warning instead of finding the undefined
- Uses value={x || ''} on a number field and loses zero
- Thinks useState's initial argument re-runs when props change
- Believes null is a valid controlled value
- Claims the warning is cosmetic with no user-visible effect