Understanding the difference between controlled and uncontrolled components is essential for React forms. Here's a comprehensive guide.
Controlled Components#
1// Controlled: React manages the state
2function ControlledInput() {
3 const [value, setValue] = useState('');
4
5 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
6 setValue(e.target.value);
7 };
8
9 return (
10 <input
11 type="text"
12 value={value}
13 onChange={handleChange}
14 />
15 );
16}
17
18// Benefits:
19// - Single source of truth
20// - Instant validation
21// - Conditional disable/enable
22// - Format input on the fly
23// - Enforce input constraints
24
25// Format input
26function PhoneInput() {
27 const [phone, setPhone] = useState('');
28
29 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
30 const digits = e.target.value.replace(/\D/g, '');
31 const formatted = digits.replace(
32 /(\d{3})(\d{3})(\d{4})/,
33 '($1) $2-$3'
34 );
35 setPhone(formatted);
36 };
37
38 return <input value={phone} onChange={handleChange} />;
39}
40
41// Enforce constraints
42function MaxLengthInput() {
43 const [value, setValue] = useState('');
44
45 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
46 if (e.target.value.length <= 10) {
47 setValue(e.target.value);
48 }
49 };
50
51 return <input value={value} onChange={handleChange} />;
52}Uncontrolled Components#
1// Uncontrolled: DOM manages the state
2function UncontrolledInput() {
3 const inputRef = useRef<HTMLInputElement>(null);
4
5 const handleSubmit = (e: React.FormEvent) => {
6 e.preventDefault();
7 console.log('Value:', inputRef.current?.value);
8 };
9
10 return (
11 <form onSubmit={handleSubmit}>
12 <input type="text" ref={inputRef} defaultValue="initial" />
13 <button type="submit">Submit</button>
14 </form>
15 );
16}
17
18// Benefits:
19// - Less code for simple forms
20// - Easier integration with non-React code
21// - Better performance for many inputs
22// - Required for file inputs
23
24// File input (must be uncontrolled)
25function FileUpload() {
26 const fileRef = useRef<HTMLInputElement>(null);
27
28 const handleSubmit = (e: React.FormEvent) => {
29 e.preventDefault();
30 const files = fileRef.current?.files;
31 if (files && files.length > 0) {
32 uploadFile(files[0]);
33 }
34 };
35
36 return (
37 <form onSubmit={handleSubmit}>
38 <input type="file" ref={fileRef} />
39 <button type="submit">Upload</button>
40 </form>
41 );
42}
43
44// Default values
45function UncontrolledForm() {
46 return (
47 <form>
48 <input type="text" defaultValue="John" />
49 <input type="checkbox" defaultChecked />
50 <select defaultValue="b">
51 <option value="a">A</option>
52 <option value="b">B</option>
53 </select>
54 </form>
55 );
56}When to Use Each#
1// Use CONTROLLED when:
2// - Instant validation
3// - Format input as user types
4// - Conditional field behavior
5// - Enforce input constraints
6// - Dynamic form fields
7// - Submit via onChange (auto-save)
8
9function ValidatedEmail() {
10 const [email, setEmail] = useState('');
11 const [error, setError] = useState('');
12
13 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
14 const value = e.target.value;
15 setEmail(value);
16
17 // Instant validation
18 if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
19 setError('Invalid email');
20 } else {
21 setError('');
22 }
23 };
24
25 return (
26 <div>
27 <input value={email} onChange={handleChange} />
28 {error && <span className="error">{error}</span>}
29 </div>
30 );
31}
32
33// Use UNCONTROLLED when:
34// - Simple forms with no validation
35// - Integration with third-party libraries
36// - Performance-critical many-input forms
37// - File uploads
38// - One-time form submission
39
40function SimpleContactForm() {
41 const formRef = useRef<HTMLFormElement>(null);
42
43 const handleSubmit = (e: React.FormEvent) => {
44 e.preventDefault();
45 const formData = new FormData(formRef.current!);
46 const data = Object.fromEntries(formData);
47 sendMessage(data);
48 };
49
50 return (
51 <form ref={formRef} onSubmit={handleSubmit}>
52 <input name="name" defaultValue="" />
53 <input name="email" type="email" defaultValue="" />
54 <textarea name="message" defaultValue="" />
55 <button type="submit">Send</button>
56 </form>
57 );
58}Hybrid Approaches#
1// Mix controlled and uncontrolled
2function HybridForm() {
3 // Controlled for fields needing validation
4 const [email, setEmail] = useState('');
5 const [emailError, setEmailError] = useState('');
6
7 // Uncontrolled for simple fields
8 const nameRef = useRef<HTMLInputElement>(null);
9 const messageRef = useRef<HTMLTextAreaElement>(null);
10
11 const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
12 setEmail(e.target.value);
13 // Validate email
14 };
15
16 const handleSubmit = (e: React.FormEvent) => {
17 e.preventDefault();
18 const data = {
19 name: nameRef.current?.value,
20 email,
21 message: messageRef.current?.value,
22 };
23 submitForm(data);
24 };
25
26 return (
27 <form onSubmit={handleSubmit}>
28 <input name="name" ref={nameRef} />
29 <input
30 name="email"
31 value={email}
32 onChange={handleEmailChange}
33 />
34 <textarea name="message" ref={messageRef} />
35 <button type="submit">Submit</button>
36 </form>
37 );
38}
39
40// Controlled with deferred updates (performance)
41function DeferredInput() {
42 const [displayValue, setDisplayValue] = useState('');
43 const [debouncedValue, setDebouncedValue] = useState('');
44
45 useEffect(() => {
46 const timer = setTimeout(() => {
47 setDebouncedValue(displayValue);
48 }, 300);
49 return () => clearTimeout(timer);
50 }, [displayValue]);
51
52 useEffect(() => {
53 // Expensive operation only on debounced value
54 if (debouncedValue) {
55 search(debouncedValue);
56 }
57 }, [debouncedValue]);
58
59 return (
60 <input
61 value={displayValue}
62 onChange={(e) => setDisplayValue(e.target.value)}
63 />
64 );
65}Form Libraries#
1// React Hook Form (uncontrolled by default)
2import { useForm } from 'react-hook-form';
3
4function HookForm() {
5 const { register, handleSubmit, formState: { errors } } = useForm();
6
7 const onSubmit = (data) => console.log(data);
8
9 return (
10 <form onSubmit={handleSubmit(onSubmit)}>
11 <input {...register('name', { required: true })} />
12 {errors.name && <span>Required</span>}
13
14 <input {...register('email', {
15 required: true,
16 pattern: /^\S+@\S+$/
17 })} />
18 {errors.email && <span>Invalid email</span>}
19
20 <button type="submit">Submit</button>
21 </form>
22 );
23}
24
25// Controlled mode with React Hook Form
26function ControlledHookForm() {
27 const { control, handleSubmit } = useForm();
28
29 return (
30 <form onSubmit={handleSubmit(onSubmit)}>
31 <Controller
32 name="email"
33 control={control}
34 rules={{ required: true }}
35 render={({ field }) => <input {...field} />}
36 />
37 </form>
38 );
39}Complex Form Patterns#
1// Dynamic form fields (controlled)
2function DynamicFields() {
3 const [fields, setFields] = useState([{ id: 1, value: '' }]);
4
5 const addField = () => {
6 setFields([...fields, { id: Date.now(), value: '' }]);
7 };
8
9 const removeField = (id: number) => {
10 setFields(fields.filter(f => f.id !== id));
11 };
12
13 const updateField = (id: number, value: string) => {
14 setFields(fields.map(f =>
15 f.id === id ? { ...f, value } : f
16 ));
17 };
18
19 return (
20 <div>
21 {fields.map(field => (
22 <div key={field.id}>
23 <input
24 value={field.value}
25 onChange={(e) => updateField(field.id, e.target.value)}
26 />
27 <button onClick={() => removeField(field.id)}>Remove</button>
28 </div>
29 ))}
30 <button onClick={addField}>Add Field</button>
31 </div>
32 );
33}
34
35// Dependent fields
36function DependentFields() {
37 const [country, setCountry] = useState('');
38 const [state, setState] = useState('');
39 const [states, setStates] = useState<string[]>([]);
40
41 useEffect(() => {
42 if (country) {
43 fetchStates(country).then(setStates);
44 setState(''); // Reset state when country changes
45 }
46 }, [country]);
47
48 return (
49 <div>
50 <select value={country} onChange={(e) => setCountry(e.target.value)}>
51 <option value="">Select Country</option>
52 <option value="US">United States</option>
53 <option value="CA">Canada</option>
54 </select>
55
56 <select
57 value={state}
58 onChange={(e) => setState(e.target.value)}
59 disabled={!country}
60 >
61 <option value="">Select State</option>
62 {states.map(s => (
63 <option key={s} value={s}>{s}</option>
64 ))}
65 </select>
66 </div>
67 );
68}Comparison Table#
| Feature | Controlled | Uncontrolled |
|------------------------|-----------------|-----------------|
| Value source | React state | DOM |
| Validation timing | Instant | On submit |
| Input formatting | Yes | No |
| Performance (many) | Can be slower | Better |
| Code complexity | More | Less |
| Third-party libs | May conflict | Easier |
| File inputs | Not possible | Required |
| Testing | Easier | Need refs |
Best Practices#
Controlled:
✓ Use for forms requiring validation
✓ Use for complex form logic
✓ Use for dependent fields
✓ Consider performance with many inputs
Uncontrolled:
✓ Use for simple forms
✓ Use for file uploads
✓ Use with third-party libs
✓ Use defaultValue, not value
General:
✓ Don't mix value and defaultValue
✓ Keep form state close to the form
✓ Consider form libraries for complex forms
✓ Use appropriate pattern for use case
Conclusion#
Controlled components offer more control for validation and formatting, while uncontrolled components are simpler and better for basic forms. Choose based on your needs: controlled for complex validation, uncontrolled for simple forms and file uploads. Consider hybrid approaches and form libraries for complex scenarios.