Controlled and uncontrolled component APIs
value means the parent owns it, defaultValue means the component does, and the owner never changes while it lives.
Predict first
Before you read: predict
ExamHall's flag note. A candidate may add a note when flagging a question; no note exists yet. Development build, no StrictMode.
The candidate types the letter c into the empty field.
What does the field show, and what does the console show?
function FlagNote() { const [note, setNote] = useState(); return <input aria-label="Flag note" value={note} onChange={(e) => setNote(e.target.value)} />;}The 30-second version
A reusable component's value has one owner. Controlled: the parent passes value and stores what onValueChange reports. Uncontrolled: the component keeps its own state, starting from defaultValue. A component can support both by treating value === undefined as "not controlled". Choose the mode at mount and keep it.
value means the parent owns it, defaultValue means the component does, and the owner never changes while it lives.
How it works
Step 1 of 10
value and onValueChange. Uncontrolled: the component holds it from defaultValue. Radix Select names both modes.Analogy: A town square's fountain, run from a console or by its own controller
| At the fountain | In React |
|---|---|
| With the console connected, a jet moves only when the operator moves it | Controlled shows what it is given (step 2) |
| A visitor presses the button on the rail; with the console connected it only lights a lamp in front of the operator, who decides | onValueChange reports; the parent decides (step 3) |
| Without the console, the fountain starts from the pattern set at switch-on, and its own controller changes it at each press of the rail button | defaultValue seeds state at mount (step 4) |
| One fountain works either way, chosen at switch-on by whether the console is connected | Supporting both, decided at mount (steps 5, 8) |
| An operator who connects the console while the fountain's own controller is running fights it for the jets | Switching owners at runtime warns (step 6) |
| With the console, every change of a jet needs the operator's hand | Control costs the parent (step 9) |
In real terms: whoever holds the value decides what shows; decide who that is before the first render.
Breaks down when: a real operator can take the jets over halfway through an afternoon; a component should never change owners.
Contrasting case: the stone statue beside the fountain, which never moves. That is a component with no state: it only shows its props.
Examples, run for real
4.1 ExamHall EXH-4120 and MR !4420: one AnswerSelect, two callers
ExamHall's AnswerSelect took only value and onValueChange. The answer page controls it: autosave stores every pick, and a Clear button must empty it. The admin results filter wrote <AnswerSelect defaultValue="all" /> and kept no state. MR !4420 supports both (step 5).
4.1 EXH-4120 controlled-only AnswerSelect: autosave saves 3 of 3 picks · admin filter picks shown 0 of 3
4.1 MR !4420 supports both: autosave saves 3 of 3 picks · admin filter picks shown 3 of 3 · Clear answer shows "none"
The filter passed no value, and nobody stored its picks (step 2). After the fix the filter keeps its own state (step 4), and the answer page still owns its value, so Clear still empties it from outside (step 10).
4.2 Media streaming: a quality menu the player must close
A player's quality menu uses the same hook for open, defaultOpen and onOpenChange. When playback starts, the page must close the menu.
4.2 defaultOpen (the menu owns it): viewer opens it -> open · Play starts -> menu open
4.2 open + onOpenChange (page owns): viewer opens it -> open · Play starts -> menu closed
The viewer opens the menu in both versions; what differs is who may close it. Only a parent that owns open can close the menu from outside (steps 1, 10).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 7
A streaming app's subtitle picker supports both modes. profile comes from the server. Development build, no StrictMode.
profile.subtitles is "English". A viewer clicks French once.
What does the picker show, and how many times did track run?
function SubtitlePicker({ value, defaultValue, onValueChange }) { const [own, setOwn] = useState(defaultValue); const isControlled = value !== undefined; const current = isControlled ? value : own; const pick = (next) => { if (!isControlled) setOwn(next); onValueChange?.(next); }; return ( <div role="group" aria-label="Subtitles"> {['English', 'French', 'Off'].map((label) => ( <button key={label} aria-pressed={current === label} onClick={() => pick(label)}>{label}</button> ))} <output>{current}</output> </div> );} <SubtitlePicker value={profile.subtitles} onValueChange={(s) => track('subtitles', s)} />Question 2 of 7
The player's settings page. QualityPicker supports both modes, as in P1. Development build, no StrictMode.
The saved quality is 1080p, but after the settings load the picker shows Auto.
Which single change makes it show 1080p after the load, and still lets the viewer pick 720p?
function PlayerSettings() { const [settings, setSettings] = useState({ quality: 'Auto', loaded: false }); useEffect(() => { loadSettings().then((s) => setSettings({ ...s, loaded: true })); }, []); return <QualityPicker defaultValue={settings.quality} />;}Question 3 of 7
Two versions of a watch page. SpeedPicker supports both modes, as in P1. EpisodeList is a plain component, not memoized. Development build, no StrictMode.
In each version the viewer clicks 1.25×, 1.5× and 2×.
How many times does EpisodeList render after the first render, in each version?
// Version 1function WatchPage() { const [speed, setSpeed] = useState(1); return ( <main> <SpeedPicker value={speed} onValueChange={setSpeed} /> <p>Playing at {speed}×</p> <EpisodeList /> </main> );}// Version 2function WatchPage() { const [speed, setSpeed] = useState(1); return ( <main> <SpeedPicker defaultValue={1} onValueChange={setSpeed} /> <p>Playing at {speed}×</p> <EpisodeList /> </main> );}Question 4 of 7
The audio-language setting. AudioPicker supports both modes, as in P1, and also logs a development warning when its mode flips. Development build, no StrictMode.
A viewer clicks Spanish, then clicks "Use my default".
What does the picker show?
function AudioSettings() { const [lang, setLang] = useState(); return ( <> <AudioPicker value={lang} defaultValue="English" onValueChange={setLang} /> <button onClick={() => setLang(undefined)}>Use my default</button> </> );}Question 5 of 7
Write AnswerSelect from a blank file. Props: options (strings), value, defaultValue, onValueChange. Render one <button role="radio"> per option, its text the option, aria-checked true on the current one. It must work uncontrolled (defaultValue; a click checks the option and calls onValueChange once with it) and controlled (value; a click calls onValueChange and changes nothing until the parent passes a new value; a new value from outside moves the check).
Export AnswerSelect.
Question 6 of 7
The catalogue's search box. Development build, no StrictMode.
A viewer types "dr", then clicks Clear.
What does the field show, and how many times does React warn that the input changed between uncontrolled and controlled?
function Search() { const [query, setQuery] = useState(''); return ( <> <input aria-label="Search" value={query || undefined} onChange={(e) => setQuery(e.target.value)} /> <button onClick={() => setQuery('')}>Clear</button> </> );}Question 7 of 7
The player's playback panel. SpeedPicker supports both modes, as in P1. profile.speed, the viewer's saved speed, is 1.25. Development build, no StrictMode.
A viewer picks 1.5×, then clicks Restore defaults: the player goes back to 1, but the picker still shows 1.5×.
Which single change makes Restore defaults show 1× in the picker, and still lets the viewer pick 2× after it?
function PlaybackPanel({ profile }) { const changeSpeed = (s) => player.setRate(s); return ( <> <SpeedPicker defaultValue={profile.speed} onValueChange={changeSpeed} /> <button onClick={() => player.setRate(1)}>Restore defaults</button> </> );}Go deeper
Pro tips
- Start controlled text at
'', neverundefined. For a value that loads later, passvalue={x ?? ''}. When: any controlled field whose state starts empty. Verify (RAN,tips.mjs): the first line below; the same note withuseState()is the opening question. - Warn when a caller switches modes. In a both-ways component, keep the first mode in a ref and log in development when it flips, as Radix's hook does. When: every component you hand to other teams. Verify (RAN,
tips.mjs): the second line below.
tip 1 useState('') note, type "c" -> field "c" · switch warnings 0
tip 2 value undefined, then "es" -> warnings 1: "Audio is changing from uncontrolled to controlled."
Source
- react.dev:
<input>, read 2026-10-13 from the page's source. "An input like<input />is uncontrolled. Even if you pass an initial value like<input defaultValue="Initial text" />, your JSX only specifies the initial value. It does not control what the value should be right now." "You cannot passvalue={undefined}first and later passvalue="some string"because React won't know whether you want the component to be uncontrolled or controlled. A controlled component should always receive a stringvalue, notnullorundefined." "either set it to an empty string ('') initially, or passvalue={someValue ?? ''}". Steps 4, 6, 8; probe, D1. Valid as of 2026-10-13. - Radix Primitives: Select, read 2026-10-13. "Can be controlled or uncontrolled." Root props
defaultValue,value("The controlled value of the select. Should be used in conjunction withonValueChange."),onValueChange,defaultOpen,open("Must be used in conjunction withonOpenChange."). Valid as of 2026-10-13.
Related lessons
Checked against the tools and docs as of 2026-10-13.