Controlled components use React state as the single source of truth for input values, while uncontrolled components let the DOM manage state and use refs to read values — each has tradeoffs for performance and complexity.
React state drives input value via value + onChange — single source of truth with full control, but re-renders on every keystroke.
DOM manages state, read via refs with defaultValue — fewer re-renders but less real-time control over input values.
Controlled for validation/dynamic forms/formatting. Uncontrolled for simple forms, file inputs, and performance-sensitive cases.
React Hook Form (uncontrolled, performant) and Formik (controlled, simpler). Both support Yup/Zod schema validation.
Server-side form handling with <form action={fn}>, useActionState, and useFormStatus — shifting toward uncontrolled patterns.
Forms in React require a deliberate choice about who owns the input state — React or the DOM. This choice affects performance, validation capabilities, and code complexity.
In a controlled component, React state drives the input value. Every keystroke triggers an onChange handler that updates state, and the input's value prop always reflects that state. React is the "single source of truth."
const [name, setName] = useState('');
<input value={name} onChange={e => setName(e.target.value)} />Advantages: instant access to the current value for validation, formatting, conditional rendering, and submission. You can enforce input rules (max length, allowed characters) by transforming the value in the handler. Every character the user types is under your control.
Disadvantage: every keystroke triggers a state update and re-render. For simple forms this is negligible, but in forms with many fields or expensive renders, it can cause noticeable lag.
In an uncontrolled component, the DOM maintains its own state. You don't set a value prop — instead, you read the value when needed using a ref:
const inputRef = useRef();
<input ref={inputRef} defaultValue="" />
<button onClick={() => console.log(inputRef.current.value)}>Submit</button>Note: use defaultValue (not value) to set the initial value without making it controlled.
Advantages: fewer re-renders (React doesn't know about keystrokes), simpler code for basic forms, required for file inputs (<input type="file" /> cannot be controlled). Disadvantage: you can't reactively validate or transform input as the user types without additional wiring.
For complex forms, libraries handle boilerplate:
register function to connect inputs and handleSubmit for validation.Field, Form, and ErrorMessage components.Both support schema validation with Yup or Zod.
React 19 introduces server-side form handling: <form action={serverAction}> submits directly to a server function. useActionState manages the form's pending/error/success states. useFormStatus lets submit buttons show loading indicators. This shifts toward uncontrolled patterns where the form data is read from the FormData object on submission rather than tracked in state.
Controlled = React state owns the input value (full control, more re-renders). Uncontrolled = DOM owns the value, read via refs (less control, fewer re-renders). Controlled for validation and dynamic forms, uncontrolled for simple forms and file inputs. React 19's form actions are pushing the ecosystem toward uncontrolled patterns with server-side processing.
Fun Fact
File inputs (<input type='file' />) can never be controlled in React because browsers don't allow JavaScript to set a file input's value for security reasons — it would enable web pages to read arbitrary files from the user's computer. This is one of the few cases where uncontrolled is the only option.