Controlled vs Uncontrolled Forms
React offers two ways to handle form inputs. Understanding the difference is essential.
Controlled Inputs
The input value is controlled by React state.
import { useState } from 'react';
function ControlledForm() {
const [name, setName] = useState('');
return (
<form>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>Hello, {name || 'Stranger'}!</p>
</form>
);
}
Characteristics:
- Input value stored in state
- State is the single source of truth
- Updates on every keystroke
- Easy to validate and control
Uncontrolled Inputs
The input manages its own state internally.
function UncontrolledForm() {
const nameRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
alert('Name: ' + nameRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={nameRef} defaultValue="" />
<button type="submit">Submit</button>
</form>
);
}
Characteristics:
- DOM holds the value
- Use refs to access value
- Simpler for simple forms
- Harder to validate
When to Use Which
| Use Case | Approach |
|---|---|
| Form validation needed | Controlled |
| Real-time field dependencies | Controlled |
| Simple form, minimal JS | Uncontrolled |
| Integrating with non-React code | Uncontrolled |
| File uploads | Uncontrolled |
| Complex multi-step forms | Controlled |
Default Values
// Controlled: set initial state
const [email, setEmail] = useState('user@example.com');
<input value={email} onChange={(e) => setEmail(e.target.value)} />
// Uncontrolled: use defaultValue
<input type="text" defaultValue="user@example.com" ref={inputRef} />
Handling Form Submission
Forms need to submit data to a server or process it with JavaScript.
Basic Form Handler
import { useState } from 'react';
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleSubmit = (e) => {
e.preventDefault(); // Prevent page reload
console.log('Form submitted:', formData);
// Send to API
fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
/>
<button type="submit">Send</button>
</form>
);
}
Why preventDefault?
// Without preventDefault: page reloads
// With preventDefault: we control what happens
const handleSubmit = (e) => {
e.preventDefault(); // Stop the default behavior
// Now we can:
// - Validate
// - Send to API
// - Update state
// - Show success message
};
Submission States
function ContactForm() {
const [formData, setFormData] = useState({ name: '', email: '' });
const [status, setStatus] = useState('idle'); // idle, submitting, success, error
const handleSubmit = async (e) => {
e.preventDefault();
setStatus('submitting');
try {
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(formData)
});
setStatus('success');
} catch (err) {
setStatus('error');
}
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button disabled={status === 'submitting'}>
{status === 'submitting' ? 'Sending...' : 'Send'}
</button>
{status === 'success' && <p>Message sent!</p>}
{status === 'error' && <p>Something went wrong.</p>}
</form>
);
}
Managing Multiple Fields
For forms with many fields, use a single state object instead of multiple useState calls.
Single Object State
function RegistrationForm() {
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
return (
<form>
<input name="firstName" value={form.firstName} onChange={handleChange} />
<input name="lastName" value={form.lastName} onChange={handleChange} />
<input name="email" type="email" value={form.email} onChange={handleChange} />
<input name="password" type="password" value={form.password} onChange={handleChange} />
<input name="confirmPassword" type="password" value={form.confirmPassword} onChange={handleChange} />
</form>
);
}
Dynamic Field Updates
// Update nested fields
const [form, setForm] = useState({
name: '',
address: {
street: '',
city: '',
zip: ''
}
});
// For nested fields, create a helper
const updateNestedField = (section, field, value) => {
setForm(prev => ({
...prev,
[section]: { ...prev[section], [field]: value }
}));
};
// Usage
<input
value={form.address.city}
onChange={(e) => updateNestedField('address', 'city', e.target.value)}
/>
Array Fields (Dynamic List)
function SkillsForm() {
const [skills, setSkills] = useState(['JavaScript', 'React']);
const addSkill = () => {
setSkills(prev => [...prev, '']);
};
const updateSkill = (index, value) => {
setSkills(prev => prev.map((skill, i) => i === index ? value : skill));
};
const removeSkill = (index) => {
setSkills(prev => prev.filter((_, i) => i !== index));
};
return (
<div>
{skills.map((skill, index) => (
<div key={index}>
<input
value={skill}
onChange={(e) => updateSkill(index, e.target.value)}
/>
<button type="button" onClick={() => removeSkill(index)}>Remove</button>
</div>
))}
<button type="button" onClick={addSkill}>Add Skill</button>
</div>
);
}
Form Validation
React forms need custom validation since controlled inputs bypass HTML5 validation.
Basic Validation
function LoginForm() {
const [form, setForm] = useState({ email: '', password: '' });
const [errors, setErrors] = useState({});
const validate = () => {
const newErrors = {};
if (!form.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(form.email)) {
newErrors.email = 'Email is invalid';
}
if (!form.password) {
newErrors.password = 'Password is required';
} else if (form.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
// Submit form
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="email"
value={form.email}
onChange={handleChange}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit">Login</button>
</form>
);
}
Real-Time Validation
// Validate on change (real-time feedback)
const handleChange = (e) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
// Validate field immediately
const fieldError = validateField(name, value);
if (fieldError) {
setErrors(prev => ({ ...prev, [name]: fieldError }));
}
};
// Or validate on blur (when user leaves field)
const handleBlur = (e) => {
const { name, value } = e.target;
const fieldError = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: fieldError }));
};
Validation Rules Object
const validationRules = {
email: {
required: true,
pattern: /\S+@\S+\.\S+/,
message: 'Please enter a valid email'
},
password: {
required: true,
minLength: 8,
message: 'Password must be at least 8 characters'
},
age: {
required: true,
min: 18,
max: 120,
message: 'Age must be between 18 and 120'
}
};
const validateField = (name, value) => {
const rules = validationRules[name];
if (!rules) return '';
if (rules.required && !value) return 'This field is required';
if (rules.pattern && !rules.pattern.test(value)) return rules.message;
if (rules.minLength && value.length < rules.minLength) return rules.message;
if (rules.min && Number(value) < rules.min) return rules.message;
if (rules.max && Number(value) > rules.max) return rules.message;
return '';
};
Advanced Form Patterns
Common patterns for building complex forms in React.
Form Component Pattern
// Reusable FormField component
function FormField({ label, name, error, children }) {
return (
<div className="form-field">
<label htmlFor={name}>{label}</label>
{children}
{error && <span className="error" role="alert">{error}</span>}
</div>
);
}
// Usage
<FormField label="Email" name="email" error={errors.email}>
<input
id="email"
name="email"
type="email"
value={form.email}
onChange={handleChange}
/>
</FormField>
Select and Checkbox Handling
function PreferencesForm() {
const [form, setForm] = useState({
newsletter: false,
theme: 'light',
interests: []
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleInterestChange = (e) => {
const { value, checked } = e.target;
setForm(prev => ({
...prev,
interests: checked
? [...prev.interests, value]
: prev.interests.filter(i => i !== value)
}));
};
return (
<form>
{/* Checkbox */}
<label>
<input
type="checkbox"
name="newsletter"
checked={form.newsletter}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
{/* Select */}
<select name="theme" value={form.theme} onChange={handleChange}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
{/* Multiple checkboxes */}
<div>
<label>
<input type="checkbox" value="tech" checked={form.interests.includes('tech')} onChange={handleInterestChange} />
Technology
</label>
<label>
<input type="checkbox" value="sports" checked={form.interests.includes('sports')} onChange={handleInterestChange} />
Sports
</label>
</div>
</form>
);
}
Form Reset
function ContactForm() {
const initialForm = { name: '', email: '', message: '' };
const [form, setForm] = useState(initialForm);
const handleSubmit = (e) => {
e.preventDefault();
// submit...
setForm(initialForm); // Reset to initial values
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button type="submit">Send</button>
<button type="button" onClick={() => setForm(initialForm)}>Reset</button>
</form>
);
}
Multi-Step Form
function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({});
const nextStep = () => setStep(s => s + 1);
const prevStep = () => setStep(s => s - 1);
return (
<div>
{step === 1 && <Step1 data={formData} setData={setFormData} onNext={nextStep} />}
{step === 2 && <Step2 data={formData} setData={setFormData} onNext={nextStep} onBack={prevStep} />}
{step === 3 && <Step3 data={formData} onBack={prevStep} onSubmit={handleSubmit} />}
</div>
);
}
Practice Problems
Create a reusable React component implementing React Forms. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for React Forms using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize React Forms for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. What is a controlled input in React?
2. Why do we call e.preventDefault() in form submission?
3. When should you use an uncontrolled input?
4. What is the advantage of using a single state object for form fields?
5. How do you handle checkbox changes in React?
Flashcards
Question
What is the difference between controlled and uncontrolled inputs?
Click to reveal answer
Answer
Controlled: value stored in React state, updated via onChange. Uncontrolled: value managed by DOM, accessed via refs. Controlled is better for validation; uncontrolled is simpler.
Question
Why must you call e.preventDefault() in onSubmit?
Click to reveal answer
Answer
Without it, the browser submits the form and reloads the page. preventDefault lets you handle submission with JavaScript instead.
Question
How do you handle multiple checkboxes in React?
Click to reveal answer
Answer
Store selected values as an array in state. On change, add the value if checked, or filter it out if unchecked.
Question
What is the pattern for a reusable FormField component?
Click to reveal answer
Answer
A component that wraps an input with a label and error message, accepting name, label, error, and children props.
Question
What is React Forms?
Click to reveal answer
Answer
React Forms is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Use controlled inputs when you need validation or field dependencies
- 2.Use uncontrolled inputs for simple forms or non-React integration
- 3.Always call e.preventDefault() in onSubmit to handle submission yourself
- 4.Group related form fields in a single state object for easier management
- 5.Validate on blur for better UX, or on change for real-time feedback
- 6.Create reusable FormField components for consistency
Interview Tips
- •Explain the difference between controlled and uncontrolled inputs
- •Know how to handle form submission with preventDefault
- •Be able to build a form with real-time validation
- •Understand how to manage dynamic form fields (add/remove items)
Cheat Sheet
React Forms Cheat Sheet
// Controlled input
const [value, setValue] = useState('');
<input value={value} onChange={(e) => setValue(e.target.value)} />
// Form submission
const handleSubmit = (e) => {
e.preventDefault();
// process form
};
<form onSubmit={handleSubmit}>
// Multiple fields
const [form, setForm] = useState({ name: '', email: '' });
const handleChange = (e) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
};