Forms in React
Controlled Components
In React, form inputs are “controlled” by component state - React controls the input value:
function NameForm() {
const [name, setName] = useState('');
return (
<form>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>Hello, {name}!</p>
</form>
);
}
Controlled = React state is the “single source of truth”
- Input
valuecomes from state - Input
onChangeupdates state - State controls what user sees
Text Inputs
Basic Text Input
function EmailForm() {
const [email, setEmail] = useState('');
function handleSubmit(event) {
event.preventDefault();
console.log('Email submitted:', email);
}
return (
<form onSubmit={handleSubmit}>
<label>
Email:
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
/>
</label>
<button type="submit">Submit</button>
</form>
);
}
Textarea
function CommentForm() {
const [comment, setComment] = useState('');
return (
<form>
<label>
Comment:
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={4}
placeholder="Write your comment..."
/>
</label>
<p>Character count: {comment.length}</p>
</form>
);
}
Select Dropdowns
Basic Select
function CountrySelect() {
const [country, setCountry] = useState('usa');
return (
<form>
<label>
Country:
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="usa">United States</option>
<option value="uk">United Kingdom</option>
<option value="canada">Canada</option>
<option value="australia">Australia</option>
</select>
</label>
<p>Selected: {country}</p>
</form>
);
}
Dynamic Options
function ProductSelector() {
const [selectedId, setSelectedId] = useState('');
const products = [
{ id: '1', name: 'Laptop', price: 999 },
{ id: '2', name: 'Mouse', price: 29 },
{ id: '3', name: 'Keyboard', price: 79 }
];
return (
<form>
<select value={selectedId} onChange={(e) => setSelectedId(e.target.value)}>
<option value="">-- Select Product --</option>
{products.map(product => (
<option key={product.id} value={product.id}>
{product.name} (${product.price})
</option>
))}
</select>
</form>
);
}
Checkboxes
Single Checkbox
function TermsCheckbox() {
const [agreed, setAgreed] = useState(false);
return (
<form>
<label>
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
I agree to the terms and conditions
</label>
<button type="submit" disabled={!agreed}>
Submit
</button>
</form>
);
}
Multiple Checkboxes
function InterestsForm() {
const [interests, setInterests] = useState([]);
function handleChange(interest) {
if (interests.includes(interest)) {
// Remove if already selected
setInterests(interests.filter(i => i !== interest));
} else {
// Add if not selected
setInterests([...interests, interest]);
}
}
return (
<form>
<p>Select your interests:</p>
<label>
<input
type="checkbox"
checked={interests.includes('sports')}
onChange={() => handleChange('sports')}
/>
Sports
</label>
<label>
<input
type="checkbox"
checked={interests.includes('music')}
onChange={() => handleChange('music')}
/>
Music
</label>
<label>
<input
type="checkbox"
checked={interests.includes('reading')}
onChange={() => handleChange('reading')}
/>
Reading
</label>
<p>Selected: {interests.join(', ') || 'None'}</p>
</form>
);
}
Radio Buttons
function SizeSelector() {
const [size, setSize] = useState('medium');
return (
<form>
<p>Select size:</p>
<label>
<input
type="radio"
value="small"
checked={size === 'small'}
onChange={(e) => setSize(e.target.value)}
/>
Small
</label>
<label>
<input
type="radio"
value="medium"
checked={size === 'medium'}
onChange={(e) => setSize(e.target.value)}
/>
Medium
</label>
<label>
<input
type="radio"
value="large"
checked={size === 'large'}
onChange={(e) => setSize(e.target.value)}
/>
Large
</label>
<p>Selected: {size}</p>
</form>
);
}
Multiple Inputs - Single State Object
Handle multiple form fields efficiently:
function SignupForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
age: '',
country: 'usa',
newsletter: false
});
function handleChange(event) {
const { name, value, type, checked } = event.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
}
function handleSubmit(event) {
event.preventDefault();
console.log('Form submitted:', formData);
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
placeholder="Username"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
placeholder="Age"
/>
<select name="country" value={formData.country} onChange={handleChange}>
<option value="usa">USA</option>
<option value="uk">UK</option>
<option value="canada">Canada</option>
</select>
<label>
<input
type="checkbox"
name="newsletter"
checked={formData.newsletter}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
<button type="submit">Sign Up</button>
</form>
);
}
Form Validation
Client-Side Validation
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
function validate() {
const newErrors = {};
if (!email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(email)) {
newErrors.email = 'Email is invalid';
}
if (!password) {
newErrors.password = 'Password is required';
} else if (password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
return newErrors;
}
function handleSubmit(event) {
event.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setErrors({});
console.log('Valid form:', { email, password });
// Submit to API
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
</div>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
{errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
</div>
<button type="submit">Log In</button>
</form>
);
}
Real-Time Validation
function EmailInput() {
const [email, setEmail] = useState('');
const [touched, setTouched] = useState(false);
const isValid = /\S+@\S+\.\S+/.test(email);
const showError = touched && !isValid && email.length > 0;
return (
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => setTouched(true)}
placeholder="Email"
style={{ borderColor: showError ? 'red' : 'gray' }}
/>
{showError && <p style={{ color: 'red' }}>Invalid email address</p>}
</div>
);
}
Form Submission
Basic Form Submission
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitStatus, setSubmitStatus] = useState(null);
function handleChange(event) {
const { name, value } = event.target;
setFormData({ ...formData, [name]: value });
}
async function handleSubmit(event) {
event.preventDefault();
setIsSubmitting(true);
setSubmitStatus(null);
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
setSubmitStatus('success');
setFormData({ name: '', email: '', message: '' });
} else {
setSubmitStatus('error');
}
} catch (error) {
setSubmitStatus('error');
} finally {
setIsSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
required
/>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
required
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
required
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
{submitStatus === 'success' && (
<p style={{ color: 'green' }}>Message sent successfully!</p>
)}
{submitStatus === 'error' && (
<p style={{ color: 'red' }}>Error sending message. Please try again.</p>
)}
</form>
);
}
File Uploads
function FileUploadForm() {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState(null);
function handleFileChange(event) {
const selectedFile = event.target.files[0];
setFile(selectedFile);
// Create preview for images
if (selectedFile && selectedFile.type.startsWith('image/')) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
};
reader.readAsDataURL(selectedFile);
}
}
async function handleSubmit(event) {
event.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData // Don't set Content-Type, browser will set it
});
if (response.ok) {
console.log('File uploaded successfully');
}
} catch (error) {
console.error('Upload failed:', error);
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="file"
onChange={handleFileChange}
accept="image/*"
/>
{preview && (
<div>
<p>Preview:</p>
<img src={preview} alt="Preview" style={{ maxWidth: '200px' }} />
</div>
)}
<button type="submit" disabled={!file}>
Upload
</button>
</form>
);
}
Common Patterns
1. Reset Form
function ResetForm() {
const initialState = { name: '', email: '' };
const [formData, setFormData] = useState(initialState);
function handleReset() {
setFormData(initialState);
}
return (
<form>
{/* inputs */}
<button type="button" onClick={handleReset}>Reset</button>
<button type="submit">Submit</button>
</form>
);
}
2. Conditional Fields
function ConditionalForm() {
const [userType, setUserType] = useState('individual');
return (
<form>
<select value={userType} onChange={(e) => setUserType(e.target.value)}>
<option value="individual">Individual</option>
<option value="business">Business</option>
</select>
{userType === 'individual' && (
<input placeholder="First Name" />
)}
{userType === 'business' && (
<>
<input placeholder="Company Name" />
<input placeholder="Tax ID" />
</>
)}
</form>
);
}
3. Multi-Step Form
function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
// step 1
name: '',
email: '',
// step 2
address: '',
city: '',
// step 3
cardNumber: ''
});
function handleNext() {
setStep(step + 1);
}
function handleBack() {
setStep(step - 1);
}
function handleSubmit(event) {
event.preventDefault();
console.log('Final submission:', formData);
}
return (
<form onSubmit={handleSubmit}>
<p>Step {step} of 3</p>
{step === 1 && (
<div>
<h2>Personal Info</h2>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Name"
/>
<input
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Email"
/>
</div>
)}
{step === 2 && (
<div>
<h2>Address</h2>
<input
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
placeholder="Address"
/>
<input
value={formData.city}
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
placeholder="City"
/>
</div>
)}
{step === 3 && (
<div>
<h2>Payment</h2>
<input
value={formData.cardNumber}
onChange={(e) => setFormData({ ...formData, cardNumber: e.target.value })}
placeholder="Card Number"
/>
</div>
)}
<div>
{step > 1 && <button type="button" onClick={handleBack}>Back</button>}
{step < 3 && <button type="button" onClick={handleNext}>Next</button>}
{step === 3 && <button type="submit">Submit</button>}
</div>
</form>
);
}
Best Practices
✅ DO:
// Use controlled components
<input value={state} onChange={(e) => setState(e.target.value)} />
// Prevent default form submission
function handleSubmit(event) {
event.preventDefault();
// ...
}
// Use name attribute for multiple inputs
<input name="email" value={formData.email} onChange={handleChange} />
// Validate before submission
function handleSubmit(event) {
event.preventDefault();
const errors = validate();
if (errors) return;
// submit
}
// Disable submit button while submitting
<button disabled={isSubmitting}>Submit</button>
// Show user feedback
{submitStatus === 'success' && <p>Success!</p>}
❌ DON’T:
// Don't forget event.preventDefault()
function handleSubmit(event) {
// Missing preventDefault - page will reload!
console.log('submitting');
}
// Don't use uncontrolled inputs without refs
<input /> // ❌ No value or onChange
// Don't forget to handle checkbox.checked
<input
type="checkbox"
value={state} // ❌ Should be checked={state}
onChange={(e) => setState(e.target.value)} // ❌ Should be e.target.checked
/>
// Don't mutate state directly
formData.email = 'new@email.com'; // ❌
setFormData({ ...formData, email: 'new@email.com' }); // ✅
// Don't submit without validation
function handleSubmit() {
// ❌ No validation
sendToAPI(formData);
}
Summary
Controlled Component Pattern:
const [value, setValue] = useState('');
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
Input Types:
- Text:
value+onChange - Checkbox:
checked+onChangewithe.target.checked - Radio:
value+checked+onChange - Select:
value+onChange
Multiple Inputs:
const [form, setForm] = useState({ name: '', email: '' });
function handleChange(e) {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
}
<input name="name" value={form.name} onChange={handleChange} />
Form Submission:
function handleSubmit(event) {
event.preventDefault(); // Prevent page reload
// Validate
// Submit to API
}
<form onSubmit={handleSubmit}>
Validation:
- Validate on submit
- Show errors after user interaction (touched)
- Disable submit while submitting
- Show success/error feedback
Next: Continue learning React hooks and patterns!