Understanding controlled and uncontrolled components is essential for React form handling. Here's a complete guide.
Controlled Components#
1import { 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// - value prop sets the displayed value
21// - onChange updates state on every keystroke
22// - Single source of truth in React stateUncontrolled Components#
1import { 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
14 type="text"
15 ref={inputRef}
16 defaultValue="initial"
17 />
18 <button type="submit">Submit</button>
19 </form>
20 );
21}
22
23// DOM controls the input value
24// - Uses defaultValue for initial value
25// - Read value via ref when needed
26// - Less React involvementControlled Form Example#
1import { useState } from 'react';
2
3function ControlledForm() {
4 const [formData, setFormData] = useState({
5 username: '',
6 email: '',
7 password: '',
8 });
9
10 const handleChange = (e) => {
11 const { name, value } = e.target;
12 setFormData(prev => ({
13 ...prev,
14 [name]: value,
15 }));
16 };
17
18 const handleSubmit = (e) => {
19 e.preventDefault();
20 console.log('Submitting:', formData);
21 };
22
23 return (
24 <form onSubmit={handleSubmit}>
25 <input
26 name="username"
27 value={formData.username}
28 onChange={handleChange}
29 placeholder="Username"
30 />
31 <input
32 name="email"
33 type="email"
34 value={formData.email}
35 onChange={handleChange}
36 placeholder="Email"
37 />
38 <input
39 name="password"
40 type="password"
41 value={formData.password}
42 onChange={handleChange}
43 placeholder="Password"
44 />
45 <button type="submit">Register</button>
46 </form>
47 );
48}Uncontrolled Form Example#
1import { useRef } from 'react';
2
3function UncontrolledForm() {
4 const formRef = useRef(null);
5
6 const handleSubmit = (e) => {
7 e.preventDefault();
8 const formData = new FormData(formRef.current);
9 const data = Object.fromEntries(formData);
10 console.log('Submitting:', data);
11 };
12
13 return (
14 <form ref={formRef} onSubmit={handleSubmit}>
15 <input name="username" defaultValue="" placeholder="Username" />
16 <input name="email" type="email" placeholder="Email" />
17 <input name="password" type="password" placeholder="Password" />
18 <button type="submit">Register</button>
19 </form>
20 );
21}When to Use Each#
1// CONTROLLED - Use when you need:
2
3// 1. Instant validation
4function ValidatedInput() {
5 const [email, setEmail] = useState('');
6 const [error, setError] = useState('');
7
8 const handleChange = (e) => {
9 const value = e.target.value;
10 setEmail(value);
11
12 if (value && !value.includes('@')) {
13 setError('Invalid email');
14 } else {
15 setError('');
16 }
17 };
18
19 return (
20 <div>
21 <input value={email} onChange={handleChange} />
22 {error && <span className="error">{error}</span>}
23 </div>
24 );
25}
26
27// 2. Conditional form fields
28function DynamicForm() {
29 const [hasPhone, setHasPhone] = useState(false);
30 const [phone, setPhone] = useState('');
31
32 return (
33 <form>
34 <label>
35 <input
36 type="checkbox"
37 checked={hasPhone}
38 onChange={(e) => setHasPhone(e.target.checked)}
39 />
40 Add phone number
41 </label>
42
43 {hasPhone && (
44 <input
45 value={phone}
46 onChange={(e) => setPhone(e.target.value)}
47 placeholder="Phone"
48 />
49 )}
50 </form>
51 );
52}
53
54// 3. Input formatting
55function PhoneInput() {
56 const [phone, setPhone] = useState('');
57
58 const formatPhone = (value) => {
59 const digits = value.replace(/\D/g, '');
60 if (digits.length <= 3) return digits;
61 if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
62 return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
63 };
64
65 const handleChange = (e) => {
66 setPhone(formatPhone(e.target.value));
67 };
68
69 return <input value={phone} onChange={handleChange} />;
70}
71
72// UNCONTROLLED - Use when:
73// - Simple forms without validation
74// - File inputs (always uncontrolled)
75// - Integration with non-React code
76// - Performance critical (many inputs)File Inputs (Always Uncontrolled)#
1function FileUpload() {
2 const fileInputRef = useRef(null);
3 const [fileName, setFileName] = useState('');
4
5 const handleChange = (e) => {
6 const file = e.target.files[0];
7 if (file) {
8 setFileName(file.name);
9 }
10 };
11
12 const handleUpload = async () => {
13 const file = fileInputRef.current.files[0];
14 if (file) {
15 const formData = new FormData();
16 formData.append('file', file);
17 await fetch('/api/upload', {
18 method: 'POST',
19 body: formData,
20 });
21 }
22 };
23
24 return (
25 <div>
26 <input
27 type="file"
28 ref={fileInputRef}
29 onChange={handleChange}
30 />
31 {fileName && <p>Selected: {fileName}</p>}
32 <button onClick={handleUpload}>Upload</button>
33 </div>
34 );
35}Mixed Approach#
1function MixedForm() {
2 // Controlled for 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');
13 return false;
14 }
15 setEmailError('');
16 return true;
17 };
18
19 const handleSubmit = (e) => {
20 e.preventDefault();
21
22 if (!validateEmail(email)) return;
23
24 const data = {
25 name: nameRef.current.value,
26 email,
27 notes: notesRef.current.value,
28 };
29
30 console.log('Submitting:', data);
31 };
32
33 return (
34 <form onSubmit={handleSubmit}>
35 <input
36 ref={nameRef}
37 defaultValue=""
38 placeholder="Name"
39 />
40
41 <input
42 value={email}
43 onChange={(e) => {
44 setEmail(e.target.value);
45 validateEmail(e.target.value);
46 }}
47 placeholder="Email"
48 />
49 {emailError && <span>{emailError}</span>}
50
51 <textarea
52 ref={notesRef}
53 defaultValue=""
54 placeholder="Notes"
55 />
56
57 <button type="submit">Submit</button>
58 </form>
59 );
60}Select Elements#
1// Controlled select
2function ControlledSelect() {
3 const [selected, setSelected] = useState('');
4
5 return (
6 <select value={selected} onChange={(e) => setSelected(e.target.value)}>
7 <option value="">Choose...</option>
8 <option value="a">Option A</option>
9 <option value="b">Option B</option>
10 </select>
11 );
12}
13
14// Multi-select
15function MultiSelect() {
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">Option A</option>
26 <option value="b">Option B</option>
27 <option value="c">Option C</option>
28 </select>
29 );
30}Checkboxes and Radio Buttons#
1// Controlled checkbox
2function ControlledCheckbox() {
3 const [checked, setChecked] = useState(false);
4
5 return (
6 <label>
7 <input
8 type="checkbox"
9 checked={checked}
10 onChange={(e) => setChecked(e.target.checked)}
11 />
12 Accept terms
13 </label>
14 );
15}
16
17// Controlled radio group
18function RadioGroup() {
19 const [selected, setSelected] = useState('');
20
21 return (
22 <div>
23 {['small', 'medium', 'large'].map(size => (
24 <label key={size}>
25 <input
26 type="radio"
27 name="size"
28 value={size}
29 checked={selected === size}
30 onChange={(e) => setSelected(e.target.value)}
31 />
32 {size}
33 </label>
34 ))}
35 </div>
36 );
37}
38
39// Checkbox group
40function CheckboxGroup() {
41 const [selected, setSelected] = useState([]);
42
43 const handleChange = (value) => {
44 setSelected(prev =>
45 prev.includes(value)
46 ? prev.filter(v => v !== value)
47 : [...prev, value]
48 );
49 };
50
51 return (
52 <div>
53 {['react', 'vue', 'angular'].map(framework => (
54 <label key={framework}>
55 <input
56 type="checkbox"
57 checked={selected.includes(framework)}
58 onChange={() => handleChange(framework)}
59 />
60 {framework}
61 </label>
62 ))}
63 </div>
64 );
65}Reset Patterns#
1// Controlled reset
2function ControlledReset() {
3 const initialState = { name: '', email: '' };
4 const [form, setForm] = useState(initialState);
5
6 const handleReset = () => {
7 setForm(initialState);
8 };
9
10 return (
11 <form>
12 <input
13 value={form.name}
14 onChange={(e) => setForm(f => ({ ...f, name: e.target.value }))}
15 />
16 <button type="button" onClick={handleReset}>Reset</button>
17 </form>
18 );
19}
20
21// Uncontrolled reset with key
22function UncontrolledReset() {
23 const [key, setKey] = useState(0);
24
25 return (
26 <div>
27 <form key={key}>
28 <input defaultValue="" />
29 </form>
30 <button onClick={() => setKey(k => k + 1)}>Reset</button>
31 </div>
32 );
33}Best Practices#
Controlled:
✓ Use for validation requirements
✓ Use for formatted inputs
✓ Use for conditional logic
✓ Easier to test
Uncontrolled:
✓ Use for file inputs
✓ Use for simple forms
✓ Use for third-party integration
✓ Better performance for many inputs
General:
✓ Don't mix value and defaultValue
✓ Use key to reset uncontrolled
✓ Consider form libraries for complex forms
✓ Be consistent within a form
Avoid:
✗ Switching between controlled/uncontrolled
✗ Setting value without onChange
✗ Using controlled for file inputs
✗ Over-engineering simple forms
Conclusion#
Controlled components give React full control over form state, enabling real-time validation and formatting. Uncontrolled components let the DOM manage values, which is simpler for basic forms. Choose based on your needs: use controlled for validation and dynamic behavior, uncontrolled for simplicity. File inputs are always uncontrolled. Consider form libraries like React Hook Form for complex scenarios.