Understanding when to use controlled versus uncontrolled components is essential for building React forms. Here's a comprehensive guide to both approaches.
Controlled Components#
1import React, { useState } from 'react';
2
3function ControlledInput() {
4 const [value, setValue] = useState('');
5
6 const handleChange = (e) => {
7 setValue(e.target.value);
8 };
9
10 return (
11 <input
12 type="text"
13 value={value}
14 onChange={handleChange}
15 />
16 );
17}
18
19// React controls the input value
20// Every keystroke triggers a re-render
21// You have full control over the input stateUncontrolled Components#
1import React, { useRef } from 'react';
2
3function UncontrolledInput() {
4 const inputRef = useRef(null);
5
6 const handleSubmit = (e) => {
7 e.preventDefault();
8 console.log('Value:', inputRef.current.value);
9 };
10
11 return (
12 <form onSubmit={handleSubmit}>
13 <input type="text" ref={inputRef} defaultValue="" />
14 <button type="submit">Submit</button>
15 </form>
16 );
17}
18
19// DOM controls the input value
20// Read value only when needed
21// Less React involvementControlled Form Example#
1function ControlledForm() {
2 const [formData, setFormData] = useState({
3 name: '',
4 email: '',
5 message: '',
6 });
7 const [errors, setErrors] = useState({});
8
9 const handleChange = (e) => {
10 const { name, value } = e.target;
11 setFormData((prev) => ({
12 ...prev,
13 [name]: value,
14 }));
15
16 // Real-time validation
17 if (name === 'email' && !value.includes('@')) {
18 setErrors((prev) => ({ ...prev, email: 'Invalid email' }));
19 } else {
20 setErrors((prev) => ({ ...prev, [name]: null }));
21 }
22 };
23
24 const handleSubmit = (e) => {
25 e.preventDefault();
26 console.log('Form data:', formData);
27 };
28
29 return (
30 <form onSubmit={handleSubmit}>
31 <div>
32 <input
33 name="name"
34 value={formData.name}
35 onChange={handleChange}
36 placeholder="Name"
37 />
38 </div>
39
40 <div>
41 <input
42 name="email"
43 value={formData.email}
44 onChange={handleChange}
45 placeholder="Email"
46 />
47 {errors.email && <span>{errors.email}</span>}
48 </div>
49
50 <div>
51 <textarea
52 name="message"
53 value={formData.message}
54 onChange={handleChange}
55 placeholder="Message"
56 />
57 </div>
58
59 <button type="submit">Send</button>
60 </form>
61 );
62}Uncontrolled Form Example#
1function UncontrolledForm() {
2 const nameRef = useRef(null);
3 const emailRef = useRef(null);
4 const messageRef = useRef(null);
5
6 const handleSubmit = (e) => {
7 e.preventDefault();
8
9 const formData = {
10 name: nameRef.current.value,
11 email: emailRef.current.value,
12 message: messageRef.current.value,
13 };
14
15 console.log('Form data:', formData);
16 };
17
18 return (
19 <form onSubmit={handleSubmit}>
20 <div>
21 <input ref={nameRef} defaultValue="" placeholder="Name" />
22 </div>
23
24 <div>
25 <input ref={emailRef} defaultValue="" placeholder="Email" />
26 </div>
27
28 <div>
29 <textarea ref={messageRef} defaultValue="" placeholder="Message" />
30 </div>
31
32 <button type="submit">Send</button>
33 </form>
34 );
35}File Inputs (Always Uncontrolled)#
1function FileInput() {
2 const fileRef = useRef(null);
3 const [fileName, setFileName] = useState('');
4
5 const handleFileChange = (e) => {
6 const file = e.target.files[0];
7 if (file) {
8 setFileName(file.name);
9 }
10 };
11
12 const handleSubmit = (e) => {
13 e.preventDefault();
14 const file = fileRef.current.files[0];
15 console.log('Selected file:', file);
16 };
17
18 return (
19 <form onSubmit={handleSubmit}>
20 <input
21 type="file"
22 ref={fileRef}
23 onChange={handleFileChange}
24 />
25 {fileName && <p>Selected: {fileName}</p>}
26 <button type="submit">Upload</button>
27 </form>
28 );
29}Hybrid Approach#
1function HybridForm() {
2 // Controlled for fields needing validation
3 const [email, setEmail] = useState('');
4 const [emailError, setEmailError] = useState('');
5
6 // Uncontrolled for simple fields
7 const nameRef = useRef(null);
8 const notesRef = useRef(null);
9
10 const validateEmail = (value) => {
11 if (!value.includes('@')) {
12 setEmailError('Invalid email address');
13 } else {
14 setEmailError('');
15 }
16 };
17
18 const handleSubmit = (e) => {
19 e.preventDefault();
20
21 const formData = {
22 name: nameRef.current.value,
23 email: email,
24 notes: notesRef.current.value,
25 };
26
27 console.log(formData);
28 };
29
30 return (
31 <form onSubmit={handleSubmit}>
32 <input
33 ref={nameRef}
34 defaultValue=""
35 placeholder="Name"
36 />
37
38 <input
39 type="email"
40 value={email}
41 onChange={(e) => {
42 setEmail(e.target.value);
43 validateEmail(e.target.value);
44 }}
45 placeholder="Email"
46 />
47 {emailError && <span>{emailError}</span>}
48
49 <textarea
50 ref={notesRef}
51 defaultValue=""
52 placeholder="Notes"
53 />
54
55 <button type="submit">Submit</button>
56 </form>
57 );
58}Controlled Select#
1function ControlledSelect() {
2 const [selected, setSelected] = useState('');
3
4 return (
5 <select value={selected} onChange={(e) => setSelected(e.target.value)}>
6 <option value="">Select an option</option>
7 <option value="option1">Option 1</option>
8 <option value="option2">Option 2</option>
9 <option value="option3">Option 3</option>
10 </select>
11 );
12}
13
14// Multi-select
15function ControlledMultiSelect() {
16 const [selected, setSelected] = useState([]);
17
18 const handleChange = (e) => {
19 const values = Array.from(e.target.selectedOptions, (opt) => opt.value);
20 setSelected(values);
21 };
22
23 return (
24 <select multiple value={selected} onChange={handleChange}>
25 <option value="a">A</option>
26 <option value="b">B</option>
27 <option value="c">C</option>
28 </select>
29 );
30}Controlled Checkbox#
1function ControlledCheckbox() {
2 const [checked, setChecked] = useState(false);
3
4 return (
5 <label>
6 <input
7 type="checkbox"
8 checked={checked}
9 onChange={(e) => setChecked(e.target.checked)}
10 />
11 Accept terms
12 </label>
13 );
14}
15
16// Multiple checkboxes
17function CheckboxGroup() {
18 const [selected, setSelected] = useState(new Set());
19
20 const handleChange = (value) => {
21 setSelected((prev) => {
22 const next = new Set(prev);
23 if (next.has(value)) {
24 next.delete(value);
25 } else {
26 next.add(value);
27 }
28 return next;
29 });
30 };
31
32 const options = ['red', 'green', 'blue'];
33
34 return (
35 <div>
36 {options.map((option) => (
37 <label key={option}>
38 <input
39 type="checkbox"
40 checked={selected.has(option)}
41 onChange={() => handleChange(option)}
42 />
43 {option}
44 </label>
45 ))}
46 </div>
47 );
48}Controlled Radio Buttons#
1function ControlledRadio() {
2 const [selected, setSelected] = useState('');
3
4 const options = [
5 { value: 'small', label: 'Small' },
6 { value: 'medium', label: 'Medium' },
7 { value: 'large', label: 'Large' },
8 ];
9
10 return (
11 <div>
12 {options.map((option) => (
13 <label key={option.value}>
14 <input
15 type="radio"
16 name="size"
17 value={option.value}
18 checked={selected === option.value}
19 onChange={(e) => setSelected(e.target.value)}
20 />
21 {option.label}
22 </label>
23 ))}
24 </div>
25 );
26}Formatting Input Values#
1function PhoneInput() {
2 const [value, setValue] = useState('');
3
4 const formatPhone = (input) => {
5 const digits = input.replace(/\D/g, '').slice(0, 10);
6 if (digits.length <= 3) return digits;
7 if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
8 return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
9 };
10
11 const handleChange = (e) => {
12 setValue(formatPhone(e.target.value));
13 };
14
15 return (
16 <input
17 type="tel"
18 value={value}
19 onChange={handleChange}
20 placeholder="(555) 123-4567"
21 />
22 );
23}
24
25// Currency input
26function CurrencyInput() {
27 const [value, setValue] = useState('');
28
29 const formatCurrency = (input) => {
30 const digits = input.replace(/\D/g, '');
31 const number = parseInt(digits || '0', 10) / 100;
32 return number.toLocaleString('en-US', {
33 style: 'currency',
34 currency: 'USD',
35 });
36 };
37
38 return (
39 <input
40 type="text"
41 value={value}
42 onChange={(e) => setValue(formatCurrency(e.target.value))}
43 placeholder="$0.00"
44 />
45 );
46}Reset Form#
1function ResettableForm() {
2 const initialState = { name: '', email: '' };
3 const [formData, setFormData] = useState(initialState);
4
5 const handleChange = (e) => {
6 const { name, value } = e.target;
7 setFormData((prev) => ({ ...prev, [name]: value }));
8 };
9
10 const handleReset = () => {
11 setFormData(initialState);
12 };
13
14 return (
15 <form>
16 <input
17 name="name"
18 value={formData.name}
19 onChange={handleChange}
20 />
21 <input
22 name="email"
23 value={formData.email}
24 onChange={handleChange}
25 />
26 <button type="button" onClick={handleReset}>
27 Reset
28 </button>
29 </form>
30 );
31}Comparison Table#
Feature | Controlled | Uncontrolled
-------------------------|-------------------|------------------
Value source | React state | DOM
Re-renders on input | Yes | No
Real-time validation | Easy | Manual
Form submission | From state | From refs
Initial value | value prop | defaultValue prop
File inputs | Not supported | Supported
Performance | More re-renders | Fewer re-renders
Testing | Easier | Requires DOM
Best Practices#
Use Controlled When:
✓ Real-time validation needed
✓ Conditional field disabling
✓ Input formatting required
✓ Dynamic form modifications
✓ Multiple inputs depend on each other
Use Uncontrolled When:
✓ Simple forms with minimal validation
✓ File inputs
✓ Performance is critical
✓ Integrating with non-React code
✓ One-time value reading
General Tips:
✓ Stay consistent within a form
✓ Use controlled for complex interactions
✓ Consider form libraries for large forms
✓ defaultValue, not value, for uncontrolled
Conclusion#
Controlled components give you full control over form state through React, enabling real-time validation, formatting, and dynamic behavior. Uncontrolled components let the DOM handle state, offering better performance for simple forms. Choose controlled for interactive forms needing validation, and uncontrolled for simple forms or file inputs. For complex forms, consider using form libraries like React Hook Form that offer the best of both approaches.