Back to Blog
ReactFormsControlled ComponentsUncontrolled Components

React Controlled vs Uncontrolled Components

Understand the difference between controlled and uncontrolled components in React and when to use each.

B
Bootspring Team
Engineering
April 21, 2019
6 min read

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 state

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

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

Share this article

Help spread the word about Bootspring