🎛️ Controlled vs Uncontrolled Inputs: Form Mastery
Master React's input patterns: When to control state, when to let the DOM handle it, and how to choose the right approach for your forms
🎯 What You'll Learn
- The fundamental difference between controlled and uncontrolled components
- When and why to use each pattern
- Form validation strategies for both approaches
- Performance implications and optimization techniques
- Hybrid approaches and advanced patterns
- Real-world form handling scenarios
- Common pitfalls and how to avoid them
🎛️ Controlled Components: React Takes Control
Understanding Controlled Components
In controlled components, React state is the "single source of truth" for form inputs. The input's value is always driven by React state, and changes flow through event handlers.
function ControlledInputBasics() {
const [inputValue, setInputValue] = useState("");
const [textareaValue, setTextareaValue] = useState("");
const [selectValue, setSelectValue] = useState("");
const [checkboxValue, setCheckboxValue] = useState(false);
const [radioValue, setRadioValue] = useState("");
// ✅ Controlled input - value comes from state
const handleInputChange = (event) => {
setInputValue(event.target.value);
};
const handleTextareaChange = (event) => {
setTextareaValue(event.target.value);
};
const handleSelectChange = (event) => {
setSelectValue(event.target.value);
};
const handleCheckboxChange = (event) => {
setCheckboxValue(event.target.checked);
};
const handleRadioChange = (event) => {
setRadioValue(event.target.value);
};
return (
<div className="controlled-demo">
<h3>Controlled Components Demo</h3>
{/* Text Input */}
<div className="form-group">
<label htmlFor="controlled-input">Text Input:</label>
<input
type="text"
id="controlled-input"
value={inputValue} // ✅ Value controlled by React state
onChange={handleInputChange} // ✅ Changes update state
placeholder="Type something..."
/>
<p>Current value: "{inputValue}"</p>
<p>Length: {inputValue.length}</p>
</div>
{/* Textarea */}
<div className="form-group">
<label htmlFor="controlled-textarea">Textarea:</label>
<textarea
id="controlled-textarea"
value={textareaValue}
onChange={handleTextareaChange}
placeholder="Enter multiple lines..."
rows={4}
/>
<p>Lines: {textareaValue.split("\n").length}</p>
<p>
Words:{" "}
{textareaValue.trim() ? textareaValue.trim().split(/\s+/).length : 0}
</p>
</div>
{/* Select Dropdown */}
<div className="form-group">
<label htmlFor="controlled-select">Select:</label>
<select
id="controlled-select"
value={selectValue}
onChange={handleSelectChange}
>
<option value="">Choose an option...</option>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
<p>Selected: {selectValue || "None"}</p>
</div>
{/* Checkbox */}
<div className="form-group">
<label>
<input
type="checkbox"
checked={checkboxValue} // ✅ Use 'checked' for checkboxes
onChange={handleCheckboxChange}
/>
Checkbox Option
</label>
<p>Checked: {checkboxValue ? "Yes" : "No"}</p>
</div>
{/* Radio Buttons */}
<div className="form-group">
<fieldset>
<legend>Radio Options:</legend>
{["radio1", "radio2", "radio3"].map((option) => (
<label key={option}>
<input
type="radio"
name="controlled-radio"
value={option}
checked={radioValue === option} // ✅ Compare with current state
onChange={handleRadioChange}
/>
{option.charAt(0).toUpperCase() + option.slice(1)}
</label>
))}
</fieldset>
<p>Selected: {radioValue || "None"}</p>
</div>
{/* Form State Summary */}
<div className="state-summary">
<h4>Current Form State:</h4>
<pre>
{JSON.stringify(
{
input: inputValue,
textarea: textareaValue,
select: selectValue,
checkbox: checkboxValue,
radio: radioValue,
},
null,
2
)}
</pre>
</div>
</div>
);
}
Advanced Controlled Component Patterns
function AdvancedControlledPatterns() {
const [formData, setFormData] = useState({
username: "",
email: "",
password: "",
confirmPassword: "",
bio: "",
skills: [],
experience: "",
newsletter: false,
});
const [formState, setFormState] = useState({
errors: {},
touched: {},
isSubmitting: false,
isDirty: false,
});
// ✅ Generic handler for all form fields
const handleFieldChange = (fieldName) => (event) => {
const { type, value, checked } = event.target;
const newValue = type === "checkbox" ? checked : value;
setFormData((prev) => ({
...prev,
[fieldName]: newValue,
}));
setFormState((prev) => ({
...prev,
isDirty: true,
touched: { ...prev.touched, [fieldName]: true },
}));
// Real-time validation
validateField(fieldName, newValue);
};
// ✅ Multi-select handler
const handleMultiSelectChange = (fieldName) => (event) => {
const { value, checked } = event.target;
setFormData((prev) => {
const currentArray = prev[fieldName] || [];
const newArray = checked
? [...currentArray, value]
: currentArray.filter((item) => item !== value);
return {
...prev,
[fieldName]: newArray,
};
});
};
// ✅ Input transformation (e.g., formatting)
const handleFormattedChange = (fieldName, formatter) => (event) => {
const rawValue = event.target.value;
const formattedValue = formatter(rawValue);
setFormData((prev) => ({
...prev,
[fieldName]: formattedValue,
}));
};
// Formatters
const formatters = {
phone: (value) => {
// Format as (XXX) XXX-XXXX
const numbers = value.replace(/\D/g, "");
if (numbers.length <= 3) return numbers;
if (numbers.length <= 6)
return `(${numbers.slice(0, 3)}) ${numbers.slice(3)}`;
return `(${numbers.slice(0, 3)}) ${numbers.slice(3, 6)}-${numbers.slice(
6,
10
)}`;
},
currency: (value) => {
// Format as currency
const numbers = value.replace(/[^\d.]/g, "");
const parts = numbers.split(".");
if (parts.length > 2) return parts[0] + "." + parts[1];
return numbers;
},
uppercase: (value) => value.toUpperCase(),
noSpaces: (value) => value.replace(/\s/g, ""),
};
// Validation
const validateField = (fieldName, value) => {
const errors = { ...formState.errors };
switch (fieldName) {
case "username":
if (!value.trim()) {
errors.username = "Username is required";
} else if (value.length < 3) {
errors.username = "Username must be at least 3 characters";
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
errors.username =
"Username can only contain letters, numbers, and underscores";
} else {
delete errors.username;
}
break;
case "email":
if (!value.trim()) {
errors.email = "Email is required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
errors.email = "Please enter a valid email address";
} else {
delete errors.email;
}
break;
case "password":
if (!value) {
errors.password = "Password is required";
} else if (value.length < 8) {
errors.password = "Password must be at least 8 characters";
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
errors.password =
"Password must contain uppercase, lowercase, and number";
} else {
delete errors.password;
}
break;
case "confirmPassword":
if (value !== formData.password) {
errors.confirmPassword = "Passwords do not match";
} else {
delete errors.confirmPassword;
}
break;
}
setFormState((prev) => ({
...prev,
errors,
}));
};
const handleSubmit = (event) => {
event.preventDefault();
// Validate all fields
Object.keys(formData).forEach((field) => {
validateField(field, formData[field]);
});
const hasErrors = Object.keys(formState.errors).length > 0;
if (!hasErrors) {
setFormState((prev) => ({ ...prev, isSubmitting: true }));
// Simulate API call
setTimeout(() => {
console.log("Form submitted:", formData);
alert("Form submitted successfully!");
setFormState((prev) => ({ ...prev, isSubmitting: false }));
}, 2000);
}
};
return (
<form onSubmit={handleSubmit} className="advanced-controlled-form">
<h3>Advanced Controlled Form Patterns</h3>
{/* Basic text input with validation */}
<div className="form-group">
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
value={formData.username}
onChange={handleFieldChange("username")}
className={formState.errors.username ? "error" : ""}
/>
{formState.errors.username && (
<span className="error-message">{formState.errors.username}</span>
)}
</div>
{/* Email with validation */}
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={formData.email}
onChange={handleFieldChange("email")}
className={formState.errors.email ? "error" : ""}
/>
{formState.errors.email && (
<span className="error-message">{formState.errors.email}</span>
)}
</div>
{/* Password with strength indicator */}
<div className="form-group">
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
value={formData.password}
onChange={handleFieldChange("password")}
className={formState.errors.password ? "error" : ""}
/>
{formState.errors.password && (
<span className="error-message">{formState.errors.password}</span>
)}
{/* Password strength indicator */}
{formData.password && (
<div className="password-strength">
<div className="strength-bar">
<div
className={`strength-fill ${
formData.password.length >= 8 &&
/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.password)
? "strong"
: formData.password.length >= 6
? "medium"
: "weak"
}`}
style={{
width: `${Math.min(
(formData.password.length / 12) * 100,
100
)}%`,
}}
/>
</div>
</div>
)}
</div>
{/* Confirm password */}
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
value={formData.confirmPassword}
onChange={handleFieldChange("confirmPassword")}
className={formState.errors.confirmPassword ? "error" : ""}
/>
{formState.errors.confirmPassword && (
<span className="error-message">
{formState.errors.confirmPassword}
</span>
)}
</div>
{/* Formatted phone input */}
<div className="form-group">
<label htmlFor="phone">Phone:</label>
<input
type="tel"
id="phone"
value={formData.phone || ""}
onChange={handleFormattedChange("phone", formatters.phone)}
placeholder="(555) 123-4567"
/>
</div>
{/* Multi-select checkboxes */}
<div className="form-group">
<fieldset>
<legend>Skills:</legend>
{["JavaScript", "React", "Node.js", "Python", "SQL"].map((skill) => (
<label key={skill}>
<input
type="checkbox"
value={skill}
checked={formData.skills.includes(skill)}
onChange={handleMultiSelectChange("skills")}
/>
{skill}
</label>
))}
</fieldset>
<p>Selected: {formData.skills.join(", ") || "None"}</p>
</div>
{/* Character-limited textarea */}
<div className="form-group">
<label htmlFor="bio">Bio (max 500 characters):</label>
<textarea
id="bio"
value={formData.bio}
onChange={(e) => {
if (e.target.value.length <= 500) {
handleFieldChange("bio")(e);
}
}}
rows={4}
placeholder="Tell us about yourself..."
/>
<div className="character-count">
{formData.bio.length}/500 characters
</div>
</div>
{/* Newsletter checkbox */}
<div className="form-group">
<label>
<input
type="checkbox"
checked={formData.newsletter}
onChange={handleFieldChange("newsletter")}
/>
Subscribe to newsletter
</label>
</div>
{/* Submit button */}
<button
type="submit"
disabled={
formState.isSubmitting || Object.keys(formState.errors).length > 0
}
className="submit-button"
>
{formState.isSubmitting ? "Submitting..." : "Submit"}
</button>
{/* Form state debug */}
<details className="form-debug">
<summary>Form State Debug</summary>
<pre>{JSON.stringify({ formData, formState }, null, 2)}</pre>
</details>
</form>
);
}
🆓 Uncontrolled Components: Let the DOM Handle It
Understanding Uncontrolled Components
Uncontrolled components let the DOM maintain its own state. You access values using refs when needed (like on form submission).
function UncontrolledInputBasics() {
// ✅ Refs to access DOM values
const inputRef = useRef(null);
const textareaRef = useRef(null);
const selectRef = useRef(null);
const checkboxRef = useRef(null);
const fileRef = useRef(null);
const [submittedData, setSubmittedData] = useState(null);
const handleSubmit = (event) => {
event.preventDefault();
// ✅ Access values through refs
const formData = {
input: inputRef.current.value,
textarea: textareaRef.current.value,
select: selectRef.current.value,
checkbox: checkboxRef.current.checked,
file: fileRef.current.files[0]?.name || "No file selected",
};
setSubmittedData(formData);
console.log("Uncontrolled form data:", formData);
};
const handleReset = () => {
// ✅ Reset form using DOM methods
inputRef.current.value = "";
textareaRef.current.value = "";
selectRef.current.value = "";
checkboxRef.current.checked = false;
fileRef.current.value = "";
setSubmittedData(null);
};
const focusInput = () => {
inputRef.current.focus();
};
return (
<div className="uncontrolled-demo">
<h3>Uncontrolled Components Demo</h3>
<form onSubmit={handleSubmit}>
{/* Text Input with default value */}
<div className="form-group">
<label htmlFor="uncontrolled-input">Text Input:</label>
<input
type="text"
id="uncontrolled-input"
ref={inputRef}
defaultValue="Default text" // ✅ Use defaultValue, not value
placeholder="Type something..."
/>
</div>
{/* Textarea */}
<div className="form-group">
<label htmlFor="uncontrolled-textarea">Textarea:</label>
<textarea
id="uncontrolled-textarea"
ref={textareaRef}
defaultValue="Default textarea content"
rows={4}
/>
</div>
{/* Select */}
<div className="form-group">
<label htmlFor="uncontrolled-select">Select:</label>
<select
id="uncontrolled-select"
ref={selectRef}
defaultValue="option2" // ✅ Default selection
>
<option value="">Choose an option...</option>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
</div>
{/* Checkbox */}
<div className="form-group">
<label>
<input
type="checkbox"
ref={checkboxRef}
defaultChecked={true} // ✅ Use defaultChecked
/>
Checkbox Option
</label>
</div>
{/* File Input (always uncontrolled) */}
<div className="form-group">
<label htmlFor="file-input">File:</label>
<input
type="file"
id="file-input"
ref={fileRef}
accept=".txt,.pdf,.doc,.docx"
/>
</div>
<div className="form-actions">
<button type="submit">Submit</button>
<button type="button" onClick={handleReset}>
Reset
</button>
<button type="button" onClick={focusInput}>
Focus Input
</button>
</div>
</form>
{/* Display submitted data */}
{submittedData && (
<div className="submitted-data">
<h4>Submitted Data:</h4>
<pre>{JSON.stringify(submittedData, null, 2)}</pre>
</div>
)}
</div>
);
}
Advanced Uncontrolled Patterns
function AdvancedUncontrolledPatterns() {
const formRef = useRef(null);
const [validationErrors, setValidationErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// ✅ Validation using FormData API
const validateForm = (formData) => {
const errors = {};
const username = formData.get("username");
const email = formData.get("email");
const password = formData.get("password");
const confirmPassword = formData.get("confirmPassword");
const age = formData.get("age");
if (!username || username.length < 3) {
errors.username = "Username must be at least 3 characters";
}
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = "Please enter a valid email";
}
if (!password || password.length < 8) {
errors.password = "Password must be at least 8 characters";
}
if (password !== confirmPassword) {
errors.confirmPassword = "Passwords do not match";
}
if (!age || parseInt(age) < 13 || parseInt(age) > 120) {
errors.age = "Please enter a valid age (13-120)";
}
return errors;
};
const handleSubmit = async (event) => {
event.preventDefault();
// ✅ Use FormData API for uncontrolled forms
const formData = new FormData(formRef.current);
// Convert FormData to regular object for easier handling
const data = Object.fromEntries(formData.entries());
// Handle checkboxes (they won't appear in FormData if unchecked)
data.newsletter = formData.has("newsletter");
// Handle multiple checkboxes
data.skills = formData.getAll("skills");
console.log("Form data:", data);
// Validate
const errors = validateForm(formData);
setValidationErrors(errors);
if (Object.keys(errors).length === 0) {
setIsSubmitting(true);
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000));
alert("Form submitted successfully!");
formRef.current.reset(); // ✅ Reset entire form
setValidationErrors({});
} catch (error) {
console.error("Submission error:", error);
} finally {
setIsSubmitting(false);
}
}
};
// ✅ Real-time validation on blur
const handleFieldBlur = (event) => {
const { name, value } = event.target;
const tempFormData = new FormData();
tempFormData.set(name, value);
// Get all current form values for cross-field validation
const currentFormData = new FormData(formRef.current);
const errors = validateForm(currentFormData);
setValidationErrors((prev) => ({
...prev,
[name]: errors[name],
}));
};
return (
<div className="advanced-uncontrolled-demo">
<h3>Advanced Uncontrolled Form Patterns</h3>
<form ref={formRef} onSubmit={handleSubmit}>
{/* Username */}
<div className="form-group">
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
name="username" // ✅ Name attribute is crucial
onBlur={handleFieldBlur}
className={validationErrors.username ? "error" : ""}
/>
{validationErrors.username && (
<span className="error-message">{validationErrors.username}</span>
)}
</div>
{/* Email */}
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
onBlur={handleFieldBlur}
className={validationErrors.email ? "error" : ""}
/>
{validationErrors.email && (
<span className="error-message">{validationErrors.email}</span>
)}
</div>
{/* Password */}
<div className="form-group">
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
onBlur={handleFieldBlur}
className={validationErrors.password ? "error" : ""}
/>
{validationErrors.password && (
<span className="error-message">{validationErrors.password}</span>
)}
</div>
{/* Confirm Password */}
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
onBlur={handleFieldBlur}
className={validationErrors.confirmPassword ? "error" : ""}
/>
{validationErrors.confirmPassword && (
<span className="error-message">
{validationErrors.confirmPassword}
</span>
)}
</div>
{/* Age */}
<div className="form-group">
<label htmlFor="age">Age:</label>
<input
type="number"
id="age"
name="age"
min="13"
max="120"
onBlur={handleFieldBlur}
className={validationErrors.age ? "error" : ""}
/>
{validationErrors.age && (
<span className="error-message">{validationErrors.age}</span>
)}
</div>
{/* Country Select */}
<div className="form-group">
<label htmlFor="country">Country:</label>
<select id="country" name="country" defaultValue="">
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="uk">United Kingdom</option>
<option value="de">Germany</option>
</select>
</div>
{/* Skills (Multiple Checkboxes) */}
<div className="form-group">
<fieldset>
<legend>Skills:</legend>
{["JavaScript", "React", "Node.js", "Python"].map((skill) => (
<label key={skill}>
<input
type="checkbox"
name="skills" // ✅ Same name for multiple values
value={skill}
/>
{skill}
</label>
))}
</fieldset>
</div>
{/* Newsletter */}
<div className="form-group">
<label>
<input type="checkbox" name="newsletter" value="yes" />
Subscribe to newsletter
</label>
</div>
{/* Bio */}
<div className="form-group">
<label htmlFor="bio">Bio:</label>
<textarea
id="bio"
name="bio"
rows={4}
placeholder="Tell us about yourself..."
/>
</div>
{/* Hidden field */}
<input type="hidden" name="formVersion" value="2.0" />
<button type="submit" disabled={isSubmitting} className="submit-button">
{isSubmitting ? "Submitting..." : "Submit"}
</button>
</form>
</div>
);
}
⚖️ Controlled vs Uncontrolled: When to Use Each
Decision Matrix
// 📊 Decision Guide:
// ✅ Use CONTROLLED when:
// - Real-time validation needed
// - Input formatting required
// - Conditional field visibility
// - Complex form interactions
// - State needs to be shared with other components
// - You need to prevent certain inputs
function ControlledUseCase() {
const [creditCard, setCreditCard] = useState("");
const [expiryDate, setExpiryDate] = useState("");
const [showCVV, setShowCVV] = useState(false);
// ✅ Real-time formatting
const handleCreditCardChange = (event) => {
let value = event.target.value.replace(/\D/g, ""); // Remove non-digits
value = value.replace(/(\d{4})(?=\d)/g, "$1 "); // Add spaces
if (value.length <= 19) {
// Limit length
setCreditCard(value);
}
};
// ✅ Conditional field visibility
useEffect(() => {
setShowCVV(creditCard.length >= 19); // Show CVV when card number is complete
}, [creditCard]);
return (
<div>
<input
type="text"
value={creditCard}
onChange={handleCreditCardChange}
placeholder="1234 5678 9012 3456"
/>
{showCVV && <input type="text" placeholder="CVV" maxLength={3} />}
</div>
);
}
// ✅ Use UNCONTROLLED when:
// - Simple forms with basic validation
// - Performance is critical (large forms)
// - Working with third-party form libraries
// - File uploads
// - You don't need real-time updates
function UncontrolledUseCase() {
const formRef = useRef();
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(formRef.current);
// ✅ Simple validation on submit
const email = formData.get("email");
if (!email.includes("@")) {
alert("Please enter a valid email");
return;
}
// Process form...
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<input type="email" name="email" required />
<input type="file" name="resume" accept=".pdf,.doc" />
<button type="submit">Submit</button>
</form>
);
}
Performance Comparison
function PerformanceComparison() {
const [renderCount, setRenderCount] = useState(0);
const [controlledValue, setControlledValue] = useState("");
const uncontrolledRef = useRef();
// Track renders
useEffect(() => {
setRenderCount((prev) => prev + 1);
});
return (
<div className="performance-demo">
<h3>Performance Comparison</h3>
<p>Component renders: {renderCount}</p>
{/* ❌ Controlled - causes re-render on every keystroke */}
<div className="controlled-section">
<h4>Controlled Input (Re-renders on every change)</h4>
<input
type="text"
value={controlledValue}
onChange={(e) => setControlledValue(e.target.value)}
placeholder="Type here - watch render count"
/>
<p>Current value: {controlledValue}</p>
</div>
{/* ✅ Uncontrolled - no re-renders during typing */}
<div className="uncontrolled-section">
<h4>Uncontrolled Input (No re-renders during typing)</h4>
<input
ref={uncontrolledRef}
type="text"
placeholder="Type here - render count stays same"
/>
<button
onClick={() => {
alert(`Value: ${uncontrolledRef.current.value}`);
}}
>
Get Value
</button>
</div>
</div>
);
}
🔄 Hybrid Approaches
Mixing Controlled and Uncontrolled
function HybridFormApproach() {
// ✅ Controlled for fields that need real-time updates
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
// ✅ Uncontrolled for simple fields
const formRef = useRef();
// ✅ Filtered results based on controlled inputs
const filteredResults = useMemo(() => {
// Simulate filtering logic
return mockData.filter(
(item) =>
item.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
(selectedCategory === "" || item.category === selectedCategory)
);
}, [searchQuery, selectedCategory]);
const handleSubmit = (event) => {
event.preventDefault();
// Get uncontrolled values
const formData = new FormData(formRef.current);
// Combine with controlled values
const submissionData = {
searchQuery,
selectedCategory,
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
};
console.log("Hybrid form submission:", submissionData);
};
return (
<div className="hybrid-form">
<h3>Hybrid Form Approach</h3>
{/* Controlled section for real-time features */}
<div className="search-section">
<h4>Search & Filter (Controlled)</h4>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search..."
/>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
>
<option value="">All Categories</option>
<option value="tech">Technology</option>
<option value="design">Design</option>
<option value="business">Business</option>
</select>
<div className="results">
<p>Found {filteredResults.length} results</p>
{/* Render filtered results */}
</div>
</div>
{/* Uncontrolled section for simple form */}
<form ref={formRef} onSubmit={handleSubmit} className="contact-form">
<h4>Contact Form (Uncontrolled)</h4>
<input type="text" name="name" placeholder="Your name" required />
<input type="email" name="email" placeholder="Your email" required />
<textarea name="message" placeholder="Your message" rows={4} required />
<button type="submit">Send Message</button>
</form>
</div>
);
}
Converting Between Patterns
function ConvertibleForm() {
const [isControlled, setIsControlled] = useState(false);
const [controlledValue, setControlledValue] = useState("");
const uncontrolledRef = useRef();
const toggleMode = () => {
if (isControlled) {
// Converting from controlled to uncontrolled
// Set the DOM value to match current state
uncontrolledRef.current.value = controlledValue;
} else {
// Converting from uncontrolled to controlled
// Set state to match current DOM value
setControlledValue(uncontrolledRef.current.value);
}
setIsControlled(!isControlled);
};
return (
<div className="convertible-form">
<h3>Convertible Form Pattern</h3>
<button onClick={toggleMode}>
Switch to {isControlled ? "Uncontrolled" : "Controlled"} Mode
</button>
<p>
Current mode:{" "}
<strong>{isControlled ? "Controlled" : "Uncontrolled"}</strong>
</p>
{isControlled ? (
<input
type="text"
value={controlledValue}
onChange={(e) => setControlledValue(e.target.value)}
placeholder="Controlled input"
/>
) : (
<input
ref={uncontrolledRef}
type="text"
defaultValue={controlledValue}
placeholder="Uncontrolled input"
/>
)}
<p>Current value: {isControlled ? controlledValue : "Check on submit"}</p>
</div>
);
}
⚠️ Common Mistakes & Anti-Patterns
1. Mixing value and defaultValue
// ❌ WRONG: Don't mix controlled and uncontrolled props
function MixedPropsError() {
const [value, setValue] = useState("");
return (
<input
value={value} // Controlled
defaultValue="initial" // ❌ This will be ignored!
onChange={(e) => setValue(e.target.value)}
/>
);
}
// ✅ CORRECT: Choose one pattern
function CorrectPatterns() {
const [value, setValue] = useState("initial"); // Start with initial value
return (
<div>
{/* Controlled */}
<input value={value} onChange={(e) => setValue(e.target.value)} />
{/* OR Uncontrolled */}
<input defaultValue="initial" ref={someRef} />
</div>
);
}
2. Forgetting to handle null/undefined values
// ❌ WRONG: Can cause "uncontrolled to controlled" warning
function NullValueError() {
const [value, setValue] = useState(null); // ❌ null makes it uncontrolled initially
return (
<input
value={value} // ❌ null -> string causes warning
onChange={(e) => setValue(e.target.value)}
/>
);
}
// ✅ CORRECT: Always use string for controlled inputs
function CorrectNullHandling() {
const [value, setValue] = useState(""); // ✅ Always string
return (
<input
value={value || ""} // ✅ Ensure it's always a string
onChange={(e) => setValue(e.target.value)}
/>
);
}
3. Inefficient controlled components
// ❌ WRONG: Expensive operations in render
function ExpensiveControlled() {
const [value, setValue] = useState("");
// ❌ Expensive operation on every keystroke
const expensiveValidation = (val) => {
// Simulate expensive operation
for (let i = 0; i < 1000000; i++) {
// Heavy computation
}
return val.length > 5;
};
return (
<div>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<p>Valid: {expensiveValidation(value) ? "Yes" : "No"}</p>
</div>
);
}
// ✅ CORRECT: Debounce expensive operations
function OptimizedControlled() {
const [value, setValue] = useState("");
const [isValid, setIsValid] = useState(false);
// ✅ Debounced validation
const debouncedValidation = useCallback(
debounce((val) => {
// Expensive operation only after user stops typing
const valid = expensiveValidation(val);
setIsValid(valid);
}, 300),
[]
);
useEffect(() => {
debouncedValidation(value);
}, [value, debouncedValidation]);
return (
<div>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<p>Valid: {isValid ? "Yes" : "No"}</p>
</div>
);
}
🧪 Mini Challenges
Challenge 1: Smart Form Component
Build a form component that:
- Automatically chooses controlled vs uncontrolled based on props
- Supports both real-time and submit-time validation
- Handles file uploads properly
- Provides a clean API for both patterns
💡 Solution
function SmartForm({
fields,
onSubmit,
realTimeValidation = false,
initialValues = {},
}) {
const [controlledValues, setControlledValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const formRef = useRef();
const isControlled =
realTimeValidation || Object.keys(initialValues).length > 0;
const validateField = (name, value, fieldConfig) => {
if (!fieldConfig.validation) return null;
for (const rule of fieldConfig.validation) {
if (!rule.test(value)) {
return rule.message;
}
}
return null;
};
const handleControlledChange = (name, fieldConfig) => (event) => {
const value =
event.target.type === "checkbox"
? event.target.checked
: event.target.value;
setControlledValues((prev) => ({ ...prev, [name]: value }));
if (realTimeValidation) {
const error = validateField(name, value, fieldConfig);
setErrors((prev) => ({ ...prev, [name]: error }));
}
};
const handleSubmit = (event) => {
event.preventDefault();
let formData;
if (isControlled) {
formData = controlledValues;
} else {
const formDataObj = new FormData(formRef.current);
formData = Object.fromEntries(formDataObj.entries());
}
// Validate all fields
const newErrors = {};
fields.forEach((field) => {
const error = validateField(field.name, formData[field.name], field);
if (error) newErrors[field.name] = error;
});
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
onSubmit(formData);
}
};
const renderField = (field) => {
const commonProps = {
id: field.name,
name: field.name,
type: field.type || "text",
placeholder: field.placeholder,
required: field.required,
};
if (isControlled) {
return (
<input
{...commonProps}
value={controlledValues[field.name] || ""}
onChange={handleControlledChange(field.name, field)}
/>
);
} else {
return (
<input
{...commonProps}
defaultValue={initialValues[field.name] || ""}
/>
);
}
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
<h3>Smart Form ({isControlled ? "Controlled" : "Uncontrolled"})</h3>
{fields.map((field) => (
<div key={field.name} className="form-group">
<label htmlFor={field.name}>{field.label}:</label>
{renderField(field)}
{errors[field.name] && (
<span className="error">{errors[field.name]}</span>
)}
</div>
))}
<button type="submit">Submit</button>
</form>
);
}
// Usage examples
function SmartFormExamples() {
const fields = [
{
name: "username",
label: "Username",
validation: [{ test: (v) => v.length >= 3, message: "Min 3 characters" }],
},
{
name: "email",
label: "Email",
type: "email",
validation: [{ test: (v) => v.includes("@"), message: "Invalid email" }],
},
];
return (
<div>
{/* Uncontrolled form */}
<SmartForm
fields={fields}
onSubmit={(data) => console.log("Uncontrolled:", data)}
/>
{/* Controlled form with real-time validation */}
<SmartForm
fields={fields}
realTimeValidation={true}
initialValues={{ username: "john" }}
onSubmit={(data) => console.log("Controlled:", data)}
/>
</div>
);
}
Challenge 2: Form State Manager Hook
Create a custom hook that:
- Manages both controlled and uncontrolled forms
- Provides validation utilities
- Handles form reset and dirty state
- Supports field-level and form-level validation
💡 Solution
function useFormState(initialValues = {}, validationRules = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isDirty, setIsDirty] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = useCallback(
(name, value) => {
const rules = validationRules[name];
if (!rules) return null;
for (const rule of rules) {
if (typeof rule === "function") {
const result = rule(value, values);
if (result !== true) return result;
} else if (rule.test && !rule.test(value, values)) {
return rule.message;
}
}
return null;
},
[validationRules, values]
);
const validateForm = useCallback(() => {
const newErrors = {};
Object.keys(validationRules).forEach((field) => {
const error = validateField(field, values[field]);
if (error) newErrors[field] = error;
});
return newErrors;
}, [validateField, values, validationRules]);
const setValue = useCallback(
(name, value) => {
setValues((prev) => ({ ...prev, [name]: value }));
setIsDirty(true);
// Clear error when user starts typing
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: null }));
}
},
[errors]
);
const setFieldTouched = useCallback((name, isTouched = true) => {
setTouched((prev) => ({ ...prev, [name]: isTouched }));
}, []);
const setFieldError = useCallback((name, error) => {
setErrors((prev) => ({ ...prev, [name]: error }));
}, []);
const handleChange = useCallback(
(name) => (event) => {
const { type, value, checked } = event.target;
const newValue = type === "checkbox" ? checked : value;
setValue(name, newValue);
},
[setValue]
);
const handleBlur = useCallback(
(name) => (event) => {
setFieldTouched(name, true);
const error = validateField(name, event.target.value);
setFieldError(name, error);
},
[validateField, setFieldTouched, setFieldError]
);
const handleSubmit = useCallback(
(onSubmit) => async (event) => {
event.preventDefault();
setIsSubmitting(true);
// Mark all fields as touched
const allTouched = Object.keys(validationRules).reduce((acc, field) => {
acc[field] = true;
return acc;
}, {});
setTouched(allTouched);
// Validate entire form
const formErrors = validateForm();
setErrors(formErrors);
if (Object.keys(formErrors).length === 0) {
try {
await onSubmit(values);
} catch (error) {
console.error("Form submission error:", error);
}
}
setIsSubmitting(false);
},
[values, validateForm, validationRules]
);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsDirty(false);
setIsSubmitting(false);
}, [initialValues]);
const getFieldProps = useCallback(
(name) => ({
value: values[name] || "",
onChange: handleChange(name),
onBlur: handleBlur(name),
error: touched[name] ? errors[name] : null,
}),
[values, handleChange, handleBlur, touched, errors]
);
return {
values,
errors,
touched,
isDirty,
isSubmitting,
setValue,
setFieldTouched,
setFieldError,
handleChange,
handleBlur,
handleSubmit,
reset,
validateForm,
getFieldProps,
isValid: Object.keys(errors).length === 0,
};
}
// Usage example
function FormWithCustomHook() {
const validationRules = {
username: [
{
test: (v) => v.length >= 3,
message: "Username must be at least 3 characters",
},
{
test: (v) => /^[a-zA-Z0-9_]+$/.test(v),
message: "Username can only contain letters, numbers, and underscores",
},
],
email: [
{
test: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
message: "Please enter a valid email",
},
],
password: [
{
test: (v) => v.length >= 8,
message: "Password must be at least 8 characters",
},
],
confirmPassword: [
(value, allValues) => {
return value === allValues.password || "Passwords do not match";
},
],
};
const form = useFormState(
{ username: "", email: "", password: "", confirmPassword: "" },
validationRules
);
const onSubmit = async (data) => {
console.log("Form submitted:", data);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1000));
alert("Form submitted successfully!");
form.reset();
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<h3>Form with Custom Hook</h3>
<div className="form-group">
<label>Username:</label>
<input
type="text"
{...form.getFieldProps("username")}
className={form.getFieldProps("username").error ? "error" : ""}
/>
{form.getFieldProps("username").error && (
<span className="error-message">
{form.getFieldProps("username").error}
</span>
)}
</div>
<div className="form-group">
<label>Email:</label>
<input
type="email"
{...form.getFieldProps("email")}
className={form.getFieldProps("email").error ? "error" : ""}
/>
{form.getFieldProps("email").error && (
<span className="error-message">
{form.getFieldProps("email").error}
</span>
)}
</div>
<div className="form-group">
<label>Password:</label>
<input
type="password"
{...form.getFieldProps("password")}
className={form.getFieldProps("password").error ? "error" : ""}
/>
{form.getFieldProps("password").error && (
<span className="error-message">
{form.getFieldProps("password").error}
</span>
)}
</div>
<div className="form-group">
<label>Confirm Password:</label>
<input
type="password"
{...form.getFieldProps("confirmPassword")}
className={form.getFieldProps("confirmPassword").error ? "error" : ""}
/>
{form.getFieldProps("confirmPassword").error && (
<span className="error-message">
{form.getFieldProps("confirmPassword").error}
</span>
)}
</div>
<div className="form-actions">
<button type="submit" disabled={!form.isValid || form.isSubmitting}>
{form.isSubmitting ? "Submitting..." : "Submit"}
</button>
<button type="button" onClick={form.reset}>
Reset
</button>
</div>
<div className="form-state">
<p>Dirty: {form.isDirty ? "Yes" : "No"}</p>
<p>Valid: {form.isValid ? "Yes" : "No"}</p>
<p>Submitting: {form.isSubmitting ? "Yes" : "No"}</p>
</div>
</form>
);
}
🎯 When and Why: Decision Framework
Quick Decision Tree
🤔 Should I use Controlled or Uncontrolled?
├── Do you need real-time validation?
│ ├── Yes → Controlled ✅
│ └── No → Continue...
│
├── Do you need to format input as user types?
│ ├── Yes → Controlled ✅
│ └── No → Continue...
│
├── Do you need conditional field visibility?
│ ├── Yes → Controlled ✅
│ └── No → Continue...
│
├── Is it a file input?
│ ├── Yes → Uncontrolled ✅ (files are always uncontrolled)
│ └── No → Continue...
│
├── Is performance critical (large forms)?
│ ├── Yes → Uncontrolled ✅
│ └── No → Continue...
│
├── Do you need to share form state with other components?
│ ├── Yes → Controlled ✅
│ └── No → Continue...
│
└── Simple form with submit-only validation?
└── Yes → Uncontrolled ✅
Performance Guidelines
// 📊 Performance Considerations:
// ✅ Controlled: Good for
// - Small to medium forms (< 20 fields)
// - Forms with complex interactions
// - Real-time features
// ✅ Uncontrolled: Good for
// - Large forms (> 20 fields)
// - Simple forms
// - Performance-critical applications
// - Third-party form libraries
// 🎯 Hybrid: Best of both worlds
// - Controlled for interactive fields
// - Uncontrolled for simple fields
// - Use refs for occasional access
🎤 Interview Insights
Common Interview Questions
-
"What's the difference between controlled and uncontrolled components?"
- Controlled: React state manages the value
- Uncontrolled: DOM manages the value, accessed via refs
- Show code examples of both patterns
-
"When would you choose controlled over uncontrolled?"
- Real-time validation
- Input formatting
- Conditional rendering
- State sharing between components
-
"What's the 'uncontrolled to controlled' warning?"
- Happens when value changes from null/undefined to string
- Always initialize controlled inputs with empty string
- Show how to fix the warning
-
"How do you handle file uploads in React?"
- File inputs are always uncontrolled
- Use refs to access FileList
- Show proper file handling patterns
-
"What are the performance implications?"
- Controlled: Re-renders on every change
- Uncontrolled: No re-renders during typing
- When to optimize with debouncing
Code Review Red Flags
// 🚨 Red Flags in Interviews:
// ❌ Mixing controlled and uncontrolled
<input value={value} defaultValue="bad" />
// ❌ Not handling null values
const [value, setValue] = useState(null);
<input value={value} /> // Warning!
// ❌ Expensive operations in render
<input onChange={() => expensiveValidation()} />
// ❌ Not using proper form submission
<button onClick={() => getValue()}> // Should use form onSubmit
// ❌ Forgetting name attributes for uncontrolled
<input ref={ref} /> // Missing name for FormData
Best Practices to Mention
// ✅ Interview-worthy patterns:
// 1. Proper controlled initialization
const [value, setValue] = useState(""); // Always string
// 2. Debounced validation
const debouncedValidate = useCallback(debounce(validateField, 300), []);
// 3. Proper form submission
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
};
// 4. Hybrid approach for performance
const useHybridForm = () => {
// Controlled for interactive fields
// Uncontrolled for simple fields
};
// 5. Proper error handling
const [errors, setErrors] = useState({});
const validateField = (name, value) => {
// Return error or null
};
🎯 Key Takeaways
Decision Matrix Summary
| Feature | Controlled | Uncontrolled |
|---|---|---|
| Real-time validation | ✅ Excellent | ❌ Limited |
| Input formatting | ✅ Excellent | ❌ Not possible |
| Performance | ⚠️ Re-renders | ✅ No re-renders |
| Simplicity | ⚠️ More code | ✅ Less code |
| File uploads | ❌ Not possible | ✅ Required |
| Form libraries | ⚠️ Some support | ✅ Full support |
| Testing | ✅ Easy to test | ⚠️ Requires DOM |
| Accessibility | ✅ Full control | ✅ Native behavior |
Mental Model
// 🧠 Think of it this way:
// Controlled = "React is the boss"
// - React state controls everything
// - Every change goes through React
// - More power, more responsibility
// Uncontrolled = "DOM is the boss"
// - DOM handles its own state
// - React checks in when needed
// - Less power, less responsibility
// Hybrid = "Shared responsibility"
// - React controls what matters
// - DOM handles the rest
// - Best of both worlds
Production Tips
- Start with uncontrolled for simple forms
- Upgrade to controlled when you need real-time features
- Use hybrid approach for complex forms
- Always validate on submit regardless of pattern
- Consider performance for large forms
- Test both patterns in your component library
Next up: useEffect Dependencies & Cleanup - Master React's most powerful hook for side effects and lifecycle management.
Previous: Handling DOM Events
💡 Pro tip: In interviews, always explain your choice between controlled and uncontrolled. Show that you understand the trade-offs and can make informed decisions based on requirements.