Back to Blog
ReactFormsInputsComponents

React Controlled vs Uncontrolled Inputs

Understand the difference between controlled and uncontrolled components in React forms.

B
Bootspring Team
Engineering
November 4, 2018
6 min read

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 state

Uncontrolled 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 involvement

Controlled 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.

Share this article

Help spread the word about Bootspring