Back to Blog
ReactFormsComponentsPatterns

React Controlled vs Uncontrolled Components

Understand controlled and uncontrolled components in React. From form handling to refs to hybrid patterns.

B
Bootspring Team
Engineering
November 13, 2020
6 min read

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.

Share this article

Help spread the word about Bootspring