Understanding the nuances between controlled and uncontrolled components is crucial for building robust React applications. While the basic concepts are straightforward, advanced scenarios require deeper consideration.
Core Concepts Recap
Controlled Components:
- Form data is handled by React state
- Single source of truth
- Updates via
onChange
handlers - Immediate validation possible
Uncontrolled Components:
- Form data is handled by the DOM
- Accessed via refs
- Better performance for simple forms
- Useful for integrating with non-React code
Advanced Scenarios
1. Hybrid Approach (Best of Both Worlds)
Sometimes you need a mix of both approaches:
function HybridInput({ defaultValue, onChange }) {
const [value, setValue] = useState(defaultValue);
const inputRef = useRef(null);
const handleChange = (e) => {
setValue(e.target.value);
onChange?.(e.target.value);
};
// Expose imperative methods
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
getValue: () => inputRef.current.value,
setValue: (val) => {
inputRef.current.value = val;
setValue(val);
}
}));
return (
<input
ref={inputRef}
value={value}
onChange={handleChange}
defaultValue={defaultValue} // Fallback for uncontrolled
/>
);
}
2. File Inputs (Always Uncontrolled)
function FileUploader({ onFilesSelected }) {
const fileInput = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
onFilesSelected(fileInput.current.files);
};
return (
<form onSubmit={handleSubmit}>
<input
type="file"
ref={fileInput}
multiple
// Cannot be controlled in React
/>
<button type="submit">Upload</button>
</form>
);
}
3. Complex Form Optimization
For forms with many fields, uncontrolled components can improve performance:
function LargeForm() {
const formRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(formRef.current);
const data = Object.fromEntries(formData.entries());
console.log('Submitted:', data);
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
{Array.from({ length: 50 }).map((_, i) => (
<input
key={i}
name={`field-${i}`}
defaultValue="" // Uncontrolled
/>
))}
<button type="submit">Submit</button>
</form>
);
}
4. Third-party Integration
When integrating with non-React libraries (e.g., jQuery plugins):
function DatePicker({ value, onChange }) {
const inputRef = useRef(null);
useEffect(() => {
// Initialize third-party date picker
const picker = $(inputRef.current).datepicker({
defaultDate: value,
onSelect: (date) => {
onChange(date);
}
});
return () => {
picker.datepicker('destroy');
};
}, []);
// Controlled sync
useEffect(() => {
if (inputRef.current) {
$(inputRef.current).datepicker('setDate', value);
}
}, [value]);
return <input ref={inputRef} />;
}
5. Dynamic Form Fields
For dynamically added fields, uncontrolled can be simpler:
function DynamicForm() {
const [fields, setFields] = useState([{ id: 1, defaultValue: '' }]);
const formRef = useRef();
const addField = () => {
setFields(prev => [...prev, {
id: Date.now(),
defaultValue: ''
}]);
};
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(formRef.current);
// Process form data
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
{fields.map(field => (
<input
key={field.id}
name={`field-${field.id}`}
defaultValue={field.defaultValue}
/>
))}
<button type="button" onClick={addField}>Add Field</button>
<button type="submit">Submit</button>
</form>
);
}
Performance Considerations
- Controlled Components:
- Re-render on every keystroke
- Can be optimized with
React.memo
and proper state management - Better for real-time validation
- Uncontrolled Components:
- Fewer re-renders
- Better for large forms
- Requires manual syncing when needed
When to Choose Which
Use Controlled When:
- You need instant validation
- Form state affects other components
- You need to programmatically control the input
- Building complex form logic
Use Uncontrolled When:
- Integrating with non-React code
- Dealing with very large forms
- Performance is critical
- Working with file inputs
- Simple forms where you only need values on submit
Advanced Pattern: State Reducer Pattern
For ultimate control over state changes in controlled components:
function inputStateReducer(state, action) {
switch (action.type) {
case 'CHANGE':
if (action.value.includes('test')) {
return state; // Prevent changes containing 'test'
}
return { ...state, value: action.value };
default:
return state;
}
}
function SmartInput({ stateReducer = inputStateReducer }) {
const [state, dispatch] = useReducer(stateReducer, { value: '' });
const handleChange = (e) => {
dispatch({ type: 'CHANGE', value: e.target.value });
};
return (
<input
value={state.value}
onChange={handleChange}
/>
);
}
TypeScript Considerations
interface ControlledInputProps {
value: string;
onChange: (value: string) => void;
}
interface UncontrolledInputProps {
defaultValue?: string;
ref?: React.Ref<HTMLInputElement>;
}
// Generic form component that can work both ways
type InputProps<T extends boolean> = {
controlled?: T;
} & (T extends true ? ControlledInputProps : UncontrolledInputProps);
function FlexibleInput<T extends boolean>({
controlled,
...props
}: InputProps<T>) {
if (controlled) {
const { value, onChange } = props as ControlledInputProps;
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
} else {
const { defaultValue, ref } = props as UncontrolledInputProps;
return <input defaultValue={defaultValue} ref={ref} />;
}
}
Accessibility Considerations
For both patterns:
- Always associate labels with inputs
- Provide proper error states and announcements
- Maintain keyboard navigation
- Ensure proper focus management
function AccessibleInput({ label, error, ...props }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
aria-invalid={!!error}
aria-describedby={error ? `${id}-error` : undefined}
{...props}
/>
{error && (
<span id={`${id}-error`} className="error">
{error}
</span>
)}
</div>
);
}
Choosing between controlled and uncontrolled components depends on your specific use case. Controlled components offer more predictability and control, while uncontrolled components can provide better performance in certain scenarios. The most advanced solutions often combine both approaches strategically.