Controlled and Uncontrolled Components in React: Key Differences and Best Practices
Controlled components store form data in React state via the value prop and onChange handlers, while uncontrolled components let the DOM manage form data using defaultValue and refs, making them ideal for simple forms or third-party integrations.
When building forms in React, developers must choose between two fundamental patterns for managing input values. According to the facebook/react source code, this decision determines whether your component relies on React's rendering pipeline or delegates to the browser's native DOM. Understanding the technical distinctions between controlled and uncontrolled components in React prevents runtime warnings and subtle bugs in production applications.
Source of Truth: React State vs. DOM
The primary distinction lies in where the current value lives. In packages/react-dom/src/__tests__/ReactDOMInput-test.js, the test suite validates that controlled inputs treat the value prop as authoritative, while uncontrolled inputs respect the DOM's internal state.
| Aspect | Controlled Component | Uncontrolled Component |
|---|---|---|
| Source of truth | React state via value prop |
Browser DOM |
| Required props | value and onChange |
defaultValue or defaultChecked |
| Value updates | Re-render with new value prop |
DOM updates internally; React does not re-render |
| Reading values | From state variable | Via ref.current.value |
If you provide a value prop without an onChange handler, React emits a warning: "You provided a value prop to a form field without an onChange handler..." as implemented in packages/react-dom/src/__tests__/ReactDOMInput-test.js around line 225.
Implementation Patterns and Code Examples
Controlled Component Pattern
Controlled components require the value prop to remain synchronized with React state. This pattern appears throughout the React test suite for validating form behavior.
function ControlledInput() {
const [name, setName] = React.useState('');
return (
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
);
}
Uncontrolled Component Pattern
Uncontrolled components use defaultValue to set the initial render value, then rely on the DOM. The React source recommends this for simple forms where you only need the value upon submission.
function UncontrolledInput() {
const inputRef = React.useRef(null);
const handleSubmit = () => {
alert('Value: ' + inputRef.current.value);
};
return (
<>
<input type="text" defaultValue="Initial" ref={inputRef} />
<button onClick={handleSubmit}>Submit</button>
</>
);
}
The Anti-Pattern: Switching Between Modes
React explicitly warns against converting a component from controlled to uncontrolled (or vice versa) during its lifecycle. The test file packages/react-dom/src/__tests__/ReactDOMInput-test.js contains validation at line 1964: "A component is changing a controlled input to be uncontrolled..."
// ❌ BAD: This component may switch modes based on props
function BadExample({ initialValue }) {
const [value, setValue] = React.useState(initialValue);
// If initialValue is undefined, this starts uncontrolled then becomes controlled
return (
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
);
}
Warnings and Edge Cases from the React Source
The React codebase contains specific validation logic for edge cases that developers frequently encounter.
Null Values: Passing null as a value prop triggers a warning from packages/react-dom-bindings/src/shared/ReactDOMNullInputValuePropHook.js. The message suggests using an empty string for controlled components or undefined for uncontrolled ones.
Conflicting Props: If you provide both value and defaultValue, React warns that the element "must be either controlled or uncontrolled" as tested in packages/react-dom/src/__tests__/ReactDOMTextarea-test.js at line 741.
Update Batching: For controlled components within layers (such as portals), React implements special batching logic in packages/react-dom/src/events/ReactDOMUpdateBatching.js at line 29 to ensure state updates propagate correctly without excessive re-renders.
Performance and Lifecycle Considerations
Controlled components participate fully in React's render cycle. Every keystroke triggers onChange → setState → render, allowing for immediate validation and formatting but potentially causing performance issues in large forms without memoization.
Uncontrolled components bypass React's state management for value updates, reducing re-render overhead. This makes them suitable for high-frequency inputs or when integrating with non-React code that expects standard DOM behavior.
Summary
- Controlled components use the
valueprop andonChangehandlers, making React state the single source of truth for form data. - Uncontrolled components rely on
defaultValueand refs, letting the DOM manage the current value and reading it only when necessary. - Never switch modes during a component's lifecycle; React emits warnings in
ReactDOMInput-test.jsand related test files when components change between controlled and uncontrolled states. - Use controlled components for complex forms requiring validation, formatting, or real-time UI updates.
- Use uncontrolled components for simple forms, file inputs, or performance-critical scenarios where minimizing re-renders is essential.
Frequently Asked Questions
Can I use both value and defaultValue on the same input?
No. React treats this as an error and warns that the input "must be either controlled or uncontrolled." According to the test suite in packages/react-dom/src/__tests__/ReactDOMTextarea-test.js at line 741, providing both props creates ambiguity about whether React or the DOM should manage the value, which can lead to unpredictable behavior.
Why does React warn when I pass null as a value prop?
Passing null as a value prop triggers a warning from packages/react-dom-bindings/src/shared/ReactDOMNullInputValuePropHook.js because React cannot determine whether you intended the component to be controlled (with an empty string) or uncontrolled (with undefined). The warning suggests using an empty string '' for controlled inputs or undefined for uncontrolled ones to clarify your intent.
How do I convert an uncontrolled component to controlled safely?
To convert an uncontrolled component to controlled without triggering the warning found in packages/react-dom/src/__tests__/ReactDOMInput-test.js at line 1964, you must ensure the component never receives undefined or null as the value prop after initialization. Initialize your state with a non-null value (such as an empty string) and maintain that type consistently throughout the component's lifecycle. Avoid conditionally rendering the value prop based on state that might revert to undefined.
Are uncontrolled components faster than controlled components?
Uncontrolled components can offer better performance for high-frequency inputs because they bypass React's state update cycle. While controlled components trigger onChange → setState → render on every keystroke, uncontrolled components allow the DOM to handle input events internally, reducing re-render overhead. However, for most applications, the performance difference is negligible unless you are processing hundreds of inputs simultaneously or implementing complex real-time formatting that would require expensive re-renders.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →