Forms & Controlled Components
Handling form inputs and controlled components
Forms in React
HTML form elements work differently in React because they naturally maintain their own state. React gives you two approaches: controlled components (React controls the form) and uncontrolled components (DOM controls the form).
Controlled vs Uncontrolled
Controlled (Recommended)
React state is the "single source of truth". Input value is controlled by React.
Uncontrolled
DOM handles the form data. Use refs to access values when needed.
Text Input (Controlled)
import { useState } from 'react';
function TextInput() {
const [name, setName] = useState('');
const handleChange = (e) => {
setName(e.target.value);
};
return (
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
value={name} {/* Controlled by state */}
onChange={handleChange} {/* Updates state on change */}
placeholder="Enter your name"
/>
<p>Hello, {name || 'stranger'}!</p>
</div>
);
}
Multiple Inputs
function MultipleInputs() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: ''
});
// Single handler for all inputs
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value // Computed property name
}));
};
return (
<form>
<input
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="First name"
/>
<input
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Last name"
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
</form>
);
}
Different Input Types
function AllInputTypes() {
const [form, setForm] = useState({
text: '',
password: '',
number: 0,
email: '',
date: '',
color: '#000000'
});
const handleChange = (e) => {
const { name, value, type } = e.target;
setForm(prev => ({
...prev,
[name]: type === 'number' ? Number(value) : value
}));
};
return (
<form>
<input type="text" name="text" value={form.text} onChange={handleChange} />
<input type="password" name="password" value={form.password} onChange={handleChange} />
<input type="number" name="number" value={form.number} onChange={handleChange} />
<input type="email" name="email" value={form.email} onChange={handleChange} />
<input type="date" name="date" value={form.date} onChange={handleChange} />
<input type="color" name="color" value={form.color} onChange={handleChange} />
</form>
);
}
Textarea
function TextareaExample() {
const [bio, setBio] = useState('');
return (
<div>
<label htmlFor="bio">Bio:</label>
<textarea
id="bio"
value={bio}
onChange={(e) => setBio(e.target.value)}
rows={4}
placeholder="Tell us about yourself"
/>
<p>Characters: {bio.length}/500</p>
</div>
);
}
Select (Dropdown)
function SelectExample() {
const [country, setCountry] = useState('');
const [languages, setLanguages] = useState([]);
return (
<div>
{/* Single select */}
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
{/* Multiple select */}
<select
multiple
value={languages}
onChange={(e) => {
const selected = Array.from(e.target.selectedOptions, opt => opt.value);
setLanguages(selected);
}}
>
<option value="js">JavaScript</option>
<option value="py">Python</option>
<option value="go">Go</option>
</select>
</div>
);
}
Checkbox
function CheckboxExample() {
const [isAgree, setIsAgree] = useState(false);
const [interests, setInterests] = useState([]);
// Toggle single checkbox
const handleAgreeChange = (e) => {
setIsAgree(e.target.checked);
};
// Handle multiple checkboxes
const handleInterestChange = (e) => {
const { value, checked } = e.target;
if (checked) {
setInterests(prev => [...prev, value]);
} else {
setInterests(prev => prev.filter(i => i !== value));
}
};
return (
<div>
{/* Single checkbox */}
<label>
<input
type="checkbox"
checked={isAgree}
onChange={handleAgreeChange}
/>
I agree to the terms
</label>
{/* Multiple checkboxes */}
<div>
<label>
<input
type="checkbox"
value="sports"
checked={interests.includes('sports')}
onChange={handleInterestChange}
/>
Sports
</label>
<label>
<input
type="checkbox"
value="music"
checked={interests.includes('music')}
onChange={handleInterestChange}
/>
Music
</label>
</div>
</div>
);
}
Radio Buttons
function RadioExample() {
const [plan, setPlan] = useState('free');
return (
<div>
<p>Select a plan:</p>
<label>
<input
type="radio"
name="plan"
value="free"
checked={plan === 'free'}
onChange={(e) => setPlan(e.target.value)}
/>
Free
</label>
<label>
<input
type="radio"
name="plan"
value="pro"
checked={plan === 'pro'}
onChange={(e) => setPlan(e.target.value)}
/>
Pro ($9/month)
</label>
<label>
<input
type="radio"
name="plan"
value="enterprise"
checked={plan === 'enterprise'}
onChange={(e) => setPlan(e.target.value)}
/>
Enterprise ($99/month)
</label>
<p>Selected plan: {plan}</p>
</div>
);
}
Form Submission
function LoginForm() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e) => {
e.preventDefault(); // Prevent page reload!
setIsSubmitting(true);
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
console.log('Login successful!');
}
} catch (error) {
console.error('Login failed:', error);
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
required
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
Form Validation
function ValidatedForm() {
const [email, setEmail] = useState('');
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = () => {
const newErrors = {};
if (!email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = 'Invalid email format';
}
return newErrors;
};
const handleBlur = () => {
setTouched({ email: true });
setErrors(validate());
};
const handleSubmit = (e) => {
e.preventDefault();
const validationErrors = validate();
setErrors(validationErrors);
setTouched({ email: true });
if (Object.keys(validationErrors).length === 0) {
console.log('Form is valid!');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={handleBlur}
className={touched.email && errors.email ? 'error' : ''}
/>
{touched.email && errors.email && (
<span className="error-message">{errors.email}</span>
)}
<button type="submit">Submit</button>
</form>
);
}
Uncontrolled Components (with useRef)
import { useRef } from 'react';
function UncontrolledForm() {
const inputRef = useRef(null);
const fileRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('Input value:', inputRef.current.value);
console.log('File:', fileRef.current.files[0]);
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} type="text" defaultValue="Initial value" />
<input ref={fileRef} type="file" />
<button type="submit">Submit</button>
</form>
);
}
🎯 Forms Best Practices
- ✓ Use controlled components for most forms
- ✓ Always call e.preventDefault() on form submit
- ✓ Use a single handler for multiple inputs with name attribute
- ✓ Validate on blur and on submit
- ✓ Show validation errors near the input
- ✓ Disable submit button while submitting
- ✓ Consider form libraries (React Hook Form, Formik) for complex forms