who owns the input's value?
Every form input has a current value. The question is who holds it:
React (in useState) or the DOM (read via useRef on demand). A
controlled input renders {'{value: text, onChange: setText}'}
so React sees every keystroke and can transform, validate, or gate it. An
uncontrolled input renders {'{defaultValue, ref: inputRef}'}
and behaves like a 1995 HTML form — React ignores it until you reach in and read
inputRef.current.value. The reset hatch for BOTH: change the component's
key to remount it fresh.
| dimension | controlled | uncontrolled |
|---|---|---|
| source of truth | React state (useState) |
the DOM (read via ref.current.value) |
| props you pass | value + onChange |
defaultValue (initial) + ref |
| every keystroke? | yes — React re-renders on each one | no — DOM updates silently, React uninvolved |
| validation / formatting | trivial — transform inside render | imperative — read on submit, then mutate |
| read on submit | text is already in scope |
inputRef.current.value |
| must be uncontrolled | — | <input type="file"> (JS can't set file value) |
1 · the three inputs you write (edit me)
2 · Babel compiles JSX → element tree
<input value={"{text.toUpperCase()}"} onChange={...}/> becomes
React.createElement('input', {'{ value: text.toUpperCase(), onChange: ... }'}).
On every setText, React re-renders and pushes the new value prop to the DOM.
The uncontrolled input has no value prop at all — just defaultValue + ref.
// (hit "compile & render" to see Babel's output)
3 · live React (the three patterns, proven)
Type in the controlled box → it uppercases and the char counter ticks. Click
"submit & read" on the uncontrolled box → it reads back the defaultValue.
Type in the key-reset box then click "reset via key" → the input is wiped because the
new key remounts it. The gold-check runs all four
assertions automatically below.
status: — · gold: empty initially → type "hello" → HELLO + 5 chars → submit → reads "hello" → key-reset → cleared
intent → pattern
| intent | pattern | why |
|---|---|---|
| validate / format each keystroke | value={text} onChange={'{e => setText(e.target.value)}'} |
controlled — transform inside render, React sees every char |
| read once on submit, ignore typing | defaultValue="x" ref={inputRef} + read .current.value |
uncontrolled — fewer re-renders, traditional HTML form feel |
| upload a file | <input type="file" ref={fileRef}/> |
file inputs MUST be uncontrolled — JS can't set .value for security |
| reset a form field | controlled: setText('') · uncontrolled: ref.current.value = '' |
or change the parent's key to remount everything fresh |
| nuclear reset (wipe all state) | <Form key={resetCounter}/>, increment on reset |
new key unmounts old tree, mounts fresh — state of every child reset |
| mix initial value + control | useState(initialValue) then render value={state} |
NEVER use defaultValue with value — pick one owner |