Controlled vs Uncontrolled — who owns the value?

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ Builds on react: useRef & the DOM (the ref mechanic the uncontrolled pattern leans on) and frontend/react: useState (the reactive model the controlled pattern is built from).

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.

dimensioncontrolleduncontrolled
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

intentpatternwhy
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