Forms
Form handling in React including controlled components, validation, form libraries, and advanced form patterns.
Controlled Components
Basic Input Handling
import { useState } from 'react';
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const handleChange = e => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
};
const handleSubmit = e => {
e.preventDefault();
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
required
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
required
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
rows={4}
/>
<button type="submit">Submit</button>
</form>
);
}
Different Input Types
function FormInputs() {
const [formData, setFormData] = useState({
text: '',
email: '',
password: '',
number: 0,
date: '',
checkbox: false,
radio: '',
select: '',
multiSelect: [],
textarea: '',
});
const handleChange = e => {
const { name, value, type, checked } = e.target;
if (type === 'checkbox') {
setFormData(prev => ({ ...prev, [name]: checked }));
} else if (type === 'number') {
setFormData(prev => ({ ...prev, [name]: parseFloat(value) || 0 }));
} else {
setFormData(prev => ({ ...prev, [name]: value }));
}
};
const handleMultiSelectChange = e => {
const { options } = e.target;
const selected = Array.from(options)
.filter(option => option.selected)
.map(option => option.value);
setFormData(prev => ({ ...prev, multiSelect: selected }));
};
return (
<form>
{/* Text input */}
<input
type="text"
name="text"
value={formData.text}
onChange={handleChange}
placeholder="Text input"
/>
{/* Email input */}
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{/* Password input */}
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
{/* Number input */}
<input
type="number"
name="number"
value={formData.number}
onChange={handleChange}
min="0"
max="100"
/>
{/* Date input */}
<input
type="date"
name="date"
value={formData.date}
onChange={handleChange}
/>
{/* Checkbox */}
<label>
<input
type="checkbox"
name="checkbox"
checked={formData.checkbox}
onChange={handleChange}
/>
Agree to terms
</label>
{/* Radio buttons */}
<fieldset>
<legend>Choose option:</legend>
{['option1', 'option2', 'option3'].map(option => (
<label key={option}>
<input
type="radio"
name="radio"
value={option}
checked={formData.radio === option}
onChange={handleChange}
/>
{option}
</label>
))}
</fieldset>
{/* Select dropdown */}
<select name="select" value={formData.select} onChange={handleChange}>
<option value="">Choose...</option>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
{/* Multi-select */}
<select
name="multiSelect"
multiple
value={formData.multiSelect}
onChange={handleMultiSelectChange}
>
<option value="a">Option A</option>
<option value="b">Option B</option>
<option value="c">Option C</option>
</select>
{/* Textarea */}
<textarea
name="textarea"
value={formData.textarea}
onChange={handleChange}
placeholder="Your message"
rows={4}
/>
</form>
);
}
Form Validation
Basic Validation
function ValidatedForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
confirmPassword: '',
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validateField = (name, value) => {
switch (name) {
case 'email':
return !value
? 'Email is required'
: !/\S+@\S+\.\S+/.test(value)
? 'Email is invalid'
: '';
case 'password':
return !value
? 'Password is required'
: value.length < 6
? 'Password must be at least 6 characters'
: '';
case 'confirmPassword':
return !value
? 'Please confirm password'
: value !== formData.password
? 'Passwords do not match'
: '';
default:
return '';
}
};
const handleChange = e => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleBlur = e => {
const { name, value } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
const error = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
};
const handleSubmit = e => {
e.preventDefault();
// Validate all fields
const newErrors = {};
Object.keys(formData).forEach(key => {
const error = validateField(key, formData[key]);
if (error) newErrors[key] = error;
});
setErrors(newErrors);
setTouched(
Object.keys(formData).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {})
);
if (Object.keys(newErrors).length === 0) {
console.log('Form is valid!', formData);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Email"
className={errors.email ? 'error' : ''}
/>
{touched.email && errors.email && (
<span className="error-message">{errors.email}</span>
)}
</div>
<div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Password"
className={errors.password ? 'error' : ''}
/>
{touched.password && errors.password && (
<span className="error-message">{errors.password}</span>
)}
</div>
<div>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Confirm Password"
className={errors.confirmPassword ? 'error' : ''}
/>
{touched.confirmPassword && errors.confirmPassword && (
<span className="error-message">{errors.confirmPassword}</span>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}
Custom Validation Hook
function useFormValidation(initialState, validationRules) {
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validate = (fieldName, value) => {
const rule = validationRules[fieldName];
if (!rule) return '';
for (const validation of rule) {
const error = validation(value, values);
if (error) return error;
}
return '';
};
const handleChange = e => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleBlur = e => {
const { name, value } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
const error = validate(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
};
const handleSubmit = onSubmit => async e => {
e.preventDefault();
setIsSubmitting(true);
const newErrors = {};
Object.keys(values).forEach(key => {
const error = validate(key, values[key]);
if (error) newErrors[key] = error;
});
setErrors(newErrors);
setTouched(
Object.keys(values).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {})
);
if (Object.keys(newErrors).length === 0) {
try {
await onSubmit(values);
} catch (error) {
console.error('Form submission error:', error);
}
}
setIsSubmitting(false);
};
const reset = () => {
setValues(initialState);
setErrors({});
setTouched({});
setIsSubmitting(false);
};
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset,
};
}
// Validation rules
const validationRules = {
email: [
value => (!value ? 'Email is required' : ''),
value => (!/\S+@\S+\.\S+/.test(value) ? 'Email is invalid' : ''),
],
password: [
value => (!value ? 'Password is required' : ''),
value => (value.length < 6 ? 'Password must be at least 6 characters' : ''),
],
};
// Usage
function LoginForm() {
const form = useFormValidation({ email: '', password: '' }, validationRules);
const onSubmit = async values => {
console.log('Login:', values);
// Handle login logic
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input
type="email"
name="email"
value={form.values.email}
onChange={form.handleChange}
onBlur={form.handleBlur}
placeholder="Email"
/>
{form.touched.email && form.errors.email && (
<span>{form.errors.email}</span>
)}
<input
type="password"
name="password"
value={form.values.password}
onChange={form.handleChange}
onBlur={form.handleBlur}
placeholder="Password"
/>
{form.touched.password && form.errors.password && (
<span>{form.errors.password}</span>
)}
<button type="submit" disabled={form.isSubmitting}>
{form.isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
Form Libraries
Formik
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
// Validation schema
const validationSchema = Yup.object({
email: Yup.string()
.email('Invalid email address')
.required('Email is required'),
password: Yup.string()
.min(6, 'Password must be at least 6 characters')
.required('Password is required'),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], 'Passwords must match')
.required('Please confirm your password'),
});
function FormikForm() {
const handleSubmit = (values, { setSubmitting, resetForm }) => {
setTimeout(() => {
console.log('Form submitted:', values);
setSubmitting(false);
resetForm();
}, 1000);
};
return (
<Formik
initialValues={{
email: '',
password: '',
confirmPassword: '',
}}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isSubmitting }) => (
<Form>
<div>
<Field type="email" name="email" placeholder="Email" />
<ErrorMessage name="email" component="div" className="error" />
</div>
<div>
<Field type="password" name="password" placeholder="Password" />
<ErrorMessage name="password" component="div" className="error" />
</div>
<div>
<Field
type="password"
name="confirmPassword"
placeholder="Confirm Password"
/>
<ErrorMessage
name="confirmPassword"
component="div"
className="error"
/>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</Form>
)}
</Formik>
);
}
React Hook Form
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
// Validation schema
const schema = yup.object({
email: yup.string().email('Invalid email').required('Email is required'),
password: yup
.string()
.min(6, 'Password must be at least 6 characters')
.required('Password is required'),
confirmPassword: yup
.string()
.oneOf([yup.ref('password')], 'Passwords must match')
.required('Please confirm password'),
});
function ReactHookForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
watch,
} = useForm({
resolver: yupResolver(schema),
});
const onSubmit = async data => {
console.log('Form data:', data);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register('email')} type="email" placeholder="Email" />
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<input
{...register('password')}
type="password"
placeholder="Password"
/>
{errors.password && (
<span className="error">{errors.password.message}</span>
)}
</div>
<div>
<input
{...register('confirmPassword')}
type="password"
placeholder="Confirm Password"
/>
{errors.confirmPassword && (
<span className="error">{errors.confirmPassword.message}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Advanced React Hook Form
import { useForm, useFieldArray, Controller } from 'react-hook-form';
function DynamicForm() {
const {
control,
register,
handleSubmit,
formState: { errors },
watch,
} = useForm({
defaultValues: {
name: '',
email: '',
skills: [{ name: '', level: 'beginner' }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'skills',
});
const watchedFields = watch();
const onSubmit = data => {
console.log('Form data:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('name', { required: 'Name is required' })}
placeholder="Name"
/>
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
type="email"
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<h3>Skills</h3>
{fields.map((field, index) => (
<div key={field.id} className="skill-item">
<input
{...register(`skills.${index}.name`, {
required: 'Skill name is required',
})}
placeholder="Skill name"
/>
<Controller
name={`skills.${index}.level`}
control={control}
render={({ field }) => (
<select {...field}>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
)}
/>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: '', level: 'beginner' })}
>
Add Skill
</button>
</div>
<button type="submit">Submit</button>
{/* Debug: Show form values */}
<pre>{JSON.stringify(watchedFields, null, 2)}</pre>
</form>
);
}
Advanced Form Patterns
Multi-step Form
function MultiStepForm() {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState({
step1: { name: '', email: '' },
step2: { address: '', city: '' },
step3: { preferences: [] },
});
const steps = [
{ title: 'Personal Info', component: Step1 },
{ title: 'Address', component: Step2 },
{ title: 'Preferences', component: Step3 },
];
const updateFormData = stepData => {
setFormData(prev => ({
...prev,
[`step${currentStep + 1}`]: stepData,
}));
};
const nextStep = () => {
if (currentStep < steps.length - 1) {
setCurrentStep(prev => prev + 1);
}
};
const prevStep = () => {
if (currentStep > 0) {
setCurrentStep(prev => prev - 1);
}
};
const handleSubmit = () => {
console.log('Final form data:', formData);
};
const CurrentStepComponent = steps[currentStep].component;
return (
<div className="multi-step-form">
<div className="step-indicator">
{steps.map((step, index) => (
<div
key={index}
className={`step ${index === currentStep ? 'active' : ''} ${index < currentStep ? 'completed' : ''}`}
>
{step.title}
</div>
))}
</div>
<div className="step-content">
<CurrentStepComponent
data={formData[`step${currentStep + 1}`]}
updateData={updateFormData}
/>
</div>
<div className="step-navigation">
{currentStep > 0 && <button onClick={prevStep}>Previous</button>}
{currentStep < steps.length - 1 ? (
<button onClick={nextStep}>Next</button>
) : (
<button onClick={handleSubmit}>Submit</button>
)}
</div>
</div>
);
}
// Step components
function Step1({ data, updateData }) {
const [stepData, setStepData] = useState(data);
const handleChange = e => {
const { name, value } = e.target;
const newData = { ...stepData, [name]: value };
setStepData(newData);
updateData(newData);
};
return (
<div>
<h2>Personal Information</h2>
<input
type="text"
name="name"
value={stepData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="email"
name="email"
value={stepData.email}
onChange={handleChange}
placeholder="Email"
/>
</div>
);
}
File Upload
import { useState, useRef } from 'react';
function FileUpload() {
const [files, setFiles] = useState([]);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState({});
const fileInputRef = useRef(null);
const handleFileSelect = e => {
const selectedFiles = Array.from(e.target.files);
const newFiles = selectedFiles.map(file => ({
file,
id: Date.now() + Math.random(),
preview: URL.createObjectURL(file),
}));
setFiles(prev => [...prev, ...newFiles]);
};
const removeFile = id => {
setFiles(prev => {
const fileToRemove = prev.find(f => f.id === id);
if (fileToRemove) {
URL.revokeObjectURL(fileToRemove.preview);
}
return prev.filter(f => f.id !== id);
});
};
const uploadFile = async fileData => {
const formData = new FormData();
formData.append('file', fileData.file);
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
const progress = (e.loaded / e.total) * 100;
setUploadProgress(prev => ({ ...prev, [fileData.id]: progress }));
}
};
return new Promise((resolve, reject) => {
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('Upload failed'));
}
};
xhr.onerror = () => reject(new Error('Upload failed'));
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
};
const handleUpload = async () => {
setUploading(true);
try {
const uploadPromises = files.map(fileData => uploadFile(fileData));
const results = await Promise.all(uploadPromises);
console.log('Upload results:', results);
// Clear files after successful upload
setFiles([]);
setUploadProgress({});
} catch (error) {
console.error('Upload error:', error);
} finally {
setUploading(false);
}
};
return (
<div className="file-upload">
<div
className="drop-zone"
onDragOver={e => e.preventDefault()}
onDrop={e => {
e.preventDefault();
const droppedFiles = Array.from(e.dataTransfer.files);
handleFileSelect({ target: { files: droppedFiles } });
}}
onClick={() => fileInputRef.current.click()}
>
<p>Drag and drop files here or click to select</p>
<input
type="file"
ref={fileInputRef}
onChange={handleFileSelect}
multiple
accept="image/*,.pdf,.doc,.docx"
style={{ display: 'none' }}
/>
</div>
{files.length > 0 && (
<div className="file-list">
{files.map(fileData => (
<div key={fileData.id} className="file-item">
<div className="file-info">
<span>{fileData.file.name}</span>
<span>{(fileData.file.size / 1024 / 1024).toFixed(2)} MB</span>
</div>
{uploadProgress[fileData.id] && (
<div className="progress-bar">
<div
className="progress"
style={{ width: `${uploadProgress[fileData.id]}%` }}
/>
</div>
)}
<button onClick={() => removeFile(fileData.id)}>Remove</button>
</div>
))}
<button
onClick={handleUpload}
disabled={uploading || files.length === 0}
>
{uploading ? 'Uploading...' : 'Upload Files'}
</button>
</div>
)}
</div>
);
}
Form Best Practices
Performance Optimization
// Debounced input for search
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
function SearchForm() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
// Perform search
console.log('Searching for:', debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
Accessibility
function AccessibleForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const [errors, setErrors] = useState({});
return (
<form>
<fieldset>
<legend>Contact Information</legend>
<div className="form-group">
<label htmlFor="name">Name *</label>
<input
id="name"
type="text"
value={formData.name}
onChange={e =>
setFormData(prev => ({ ...prev, name: e.target.value }))
}
required
aria-invalid={errors.name ? 'true' : 'false'}
aria-describedby={errors.name ? 'name-error' : undefined}
/>
{errors.name && (
<div id="name-error" className="error" role="alert">
{errors.name}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="email">Email *</label>
<input
id="email"
type="email"
value={formData.email}
onChange={e =>
setFormData(prev => ({ ...prev, email: e.target.value }))
}
required
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<div id="email-error" className="error" role="alert">
{errors.email}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea
id="message"
value={formData.message}
onChange={e =>
setFormData(prev => ({ ...prev, message: e.target.value }))
}
rows={4}
aria-describedby="message-help"
/>
<div id="message-help" className="help-text">
Optional: Tell us more about your inquiry
</div>
</div>
<button type="submit" aria-describedby="submit-help">
Send Message
</button>
<div id="submit-help" className="help-text">
Required fields are marked with *
</div>
</fieldset>
</form>
);
}
Quick Reference
Controlled Component
const [value, setValue] = useState('');
<input type="text" value={value} onChange={e => setValue(e.target.value)} />;
Form Validation
const [errors, setErrors] = useState({});
const validate = (name, value) => {
// Validation logic
return error;
};
const handleBlur = e => {
const error = validate(e.target.name, e.target.value);
setErrors(prev => ({ ...prev, [e.target.name]: error }));
};
Form Submission
const handleSubmit = e => {
e.preventDefault();
// Process form data
};
<form onSubmit={handleSubmit}>
{/* form fields */}
<button type="submit">Submit</button>
</form>;
React Hook Form
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name', { required: 'Name is required' })} />
{errors.name && <span>{errors.name.message}</span>}
</form>;