React Forms - Complete Guide
Forms are essential for user interaction in web applications. React provides powerful tools for creating and managing forms, from simple input fields to complex multi-step forms. This guide covers everything you need to know about building forms in React.
Controlled vs Uncontrolled Components
In React, form elements can be either controlled or uncontrolled. Understanding the difference is crucial for building effective forms.
Controlled Components
Controlled components have their value controlled by React state:
function ControlledForm() {
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
alert(`Submitted name: ${name}`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<button type="submit">Submit</button>
</form>
);
}Uncontrolled Components
Uncontrolled components maintain their own internal state:
function UncontrolledForm() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
alert(`Submitted name: ${inputRef.current.value}`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
ref={inputRef}
defaultValue=""
/>
</label>
<button type="submit">Submit</button>
</form>
);
}When to use each:
- Use controlled components for most forms (better validation, control)
- Use uncontrolled components for simple cases or when integrating with non-React libraries
Building Basic Forms
Simple Input Form
function SimpleForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form data:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>First Name:</label>
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
</div>
<div>
<label>Last Name:</label>
<input
type="text"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<button type="submit">Submit</button>
</form>
);
}Different Input Types
function VariousInputs() {
const [formData, setFormData] = useState({
text: '',
password: '',
number: '',
date: '',
select: '',
checkbox: false,
radio: '',
textarea: ''
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
return (
<form>
<div>
<label>Text Input:</label>
<input
type="text"
name="text"
value={formData.text}
onChange={handleChange}
/>
</div>
<div>
<label>Password:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
</div>
<div>
<label>Number:</label>
<input
type="number"
name="number"
value={formData.number}
onChange={handleChange}
/>
</div>
<div>
<label>Date:</label>
<input
type="date"
name="date"
value={formData.date}
onChange={handleChange}
/>
</div>
<div>
<label>Select:</label>
<select
name="select"
value={formData.select}
onChange={handleChange}
>
<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>
<div>
<label>
<input
type="checkbox"
name="checkbox"
checked={formData.checkbox}
onChange={handleChange}
/>
I agree to the terms
</label>
</div>
<div>
<label>Radio Options:</label>
<label>
<input
type="radio"
name="radio"
value="option1"
checked={formData.radio === 'option1'}
onChange={handleChange}
/>
Option 1
</label>
<label>
<input
type="radio"
name="radio"
value="option2"
checked={formData.radio === 'option2'}
onChange={handleChange}
/>
Option 2
</label>
</div>
<div>
<label>Textarea:</label>
<textarea
name="textarea"
value={formData.textarea}
onChange={handleChange}
rows="4"
cols="50"
/>
</div>
</form>
);
}Form Validation
Basic Validation
function ValidatedForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
age: ''
});
const [errors, setErrors] = useState({});
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
} else if (formData.name.length < 2) {
newErrors.name = 'Name must be at least 2 characters';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.age) {
newErrors.age = 'Age is required';
} else if (isNaN(formData.age) || formData.age < 18) {
newErrors.age = 'You must be at least 18 years old';
}
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = validateForm();
if (Object.keys(newErrors).length === 0) {
console.log('Form submitted:', formData);
setErrors({});
} else {
setErrors(newErrors);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Clear error for this field when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div>
<label>Age:</label>
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
className={errors.age ? 'error' : ''}
/>
{errors.age && <span className="error-message">{errors.age}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
}Real-time Validation
function RealTimeValidation() {
const [formData, setFormData] = useState({
username: '',
password: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validateField = (name, value) => {
const newErrors = { ...errors };
switch (name) {
case 'username':
if (!value) {
newErrors.username = 'Username is required';
} else if (value.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
newErrors.username = 'Username can only contain letters, numbers, and underscores';
} else {
delete newErrors.username;
}
break;
case 'password':
if (!value) {
newErrors.password = 'Password is required';
} else if (value.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
newErrors.password = 'Password must contain at least one uppercase letter, one lowercase letter, and one number';
} else {
delete newErrors.password;
}
break;
default:
break;
}
return newErrors;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (touched[name]) {
setErrors(validateField(name, value));
}
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
setErrors(validateField(name, formData[name]));
};
const handleSubmit = (e) => {
e.preventDefault();
// Validate all fields
const allErrors = Object.keys(formData).reduce((acc, field) => {
return validateField(field, formData[field]);
}, {});
setErrors(allErrors);
if (Object.keys(allErrors).length === 0) {
console.log('Form submitted:', formData);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Username:</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
onBlur={handleBlur}
className={touched.username && errors.username ? 'error' : ''}
/>
{touched.username && errors.username && (
<span className="error-message">{errors.username}</span>
)}
</div>
<div>
<label>Password:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
className={touched.password && errors.password ? 'error' : ''}
/>
{touched.password && errors.password && (
<span className="error-message">{errors.password}</span>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}Advanced Form Patterns
Multi-step Form
function MultiStepForm() {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
personal: {
firstName: '',
lastName: '',
email: ''
},
address: {
street: '',
city: '',
zipCode: ''
},
preferences: {
newsletter: false,
notifications: true
}
});
const nextStep = () => setStep(step + 1);
const prevStep = () => setStep(step - 1);
const handleChange = (section, field, value) => {
setFormData(prev => ({
...prev,
[section]: {
...prev[section],
[field]: value
}
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Final form data:', formData);
};
const renderStep = () => {
switch (step) {
case 1:
return (
<PersonalInfoStep
data={formData.personal}
onChange={(field, value) => handleChange('personal', field, value)}
/>
);
case 2:
return (
<AddressStep
data={formData.address}
onChange={(field, value) => handleChange('address', field, value)}
/>
);
case 3:
return (
<PreferencesStep
data={formData.preferences}
onChange={(field, value) => handleChange('preferences', field, value)}
/>
);
default:
return null;
}
};
return (
<div>
<h2>Step {step} of 3</h2>
<form onSubmit={step === 3 ? handleSubmit : (e) => { e.preventDefault(); nextStep(); }}>
{renderStep()}
<div className="step-controls">
{step > 1 && (
<button type="button" onClick={prevStep}>
Previous
</button>
)}
<button type="submit">
{step === 3 ? 'Submit' : 'Next'}
</button>
</div>
</form>
</div>
);
}
function PersonalInfoStep({ data, onChange }) {
return (
<div>
<h3>Personal Information</h3>
<div>
<label>First Name:</label>
<input
type="text"
value={data.firstName}
onChange={(e) => onChange('firstName', e.target.value)}
/>
</div>
<div>
<label>Last Name:</label>
<input
type="text"
value={data.lastName}
onChange={(e) => onChange('lastName', e.target.value)}
/>
</div>
<div>
<label>Email:</label>
<input
type="email"
value={data.email}
onChange={(e) => onChange('email', e.target.value)}
/>
</div>
</div>
);
}
function AddressStep({ data, onChange }) {
return (
<div>
<h3>Address Information</h3>
<div>
<label>Street:</label>
<input
type="text"
value={data.street}
onChange={(e) => onChange('street', e.target.value)}
/>
</div>
<div>
<label>City:</label>
<input
type="text"
value={data.city}
onChange={(e) => onChange('city', e.target.value)}
/>
</div>
<div>
<label>Zip Code:</label>
<input
type="text"
value={data.zipCode}
onChange={(e) => onChange('zipCode', e.target.value)}
/>
</div>
</div>
);
}
function PreferencesStep({ data, onChange }) {
return (
<div>
<h3>Preferences</h3>
<div>
<label>
<input
type="checkbox"
checked={data.newsletter}
onChange={(e) => onChange('newsletter', e.target.checked)}
/>
Subscribe to newsletter
</label>
</div>
<div>
<label>
<input
type="checkbox"
checked={data.notifications}
onChange={(e) => onChange('notifications', e.target.checked)}
/>
Enable email notifications
</label>
</div>
</div>
);
}Dynamic Form Fields
function DynamicForm() {
const [fields, setFields] = useState([
{ id: 1, name: '', type: 'text' }
]);
const addField = () => {
const newField = {
id: Date.now(),
name: '',
type: 'text'
};
setFields([...fields, newField]);
};
const removeField = (id) => {
setFields(fields.filter(field => field.id !== id));
};
const updateField = (id, property, value) => {
setFields(fields.map(field =>
field.id === id ? { ...field, [property]: value } : field
));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Dynamic form data:', fields);
};
return (
<form onSubmit={handleSubmit}>
<h2>Dynamic Form</h2>
{fields.map((field, index) => (
<div key={field.id} className="dynamic-field">
<span>Field {index + 1}</span>
<input
type="text"
placeholder="Field name"
value={field.name}
onChange={(e) => updateField(field.id, 'name', e.target.value)}
/>
<select
value={field.type}
onChange={(e) => updateField(field.id, 'type', e.target.value)}
>
<option value="text">Text</option>
<option value="number">Number</option>
<option value="email">Email</option>
<option value="password">Password</option>
</select>
<button
type="button"
onClick={() => removeField(field.id)}
disabled={fields.length === 1}
>
Remove
</button>
</div>
))}
<button type="button" onClick={addField}>
Add Field
</button>
<button type="submit">Submit</button>
</form>
);
}Custom Form Hooks
useForm Hook
function useForm(initialValues, validationSchema) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const setValue = useCallback((name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
}, []);
const handleChange = useCallback((e) => {
const { name, value, type, checked } = e.target;
const fieldValue = type === 'checkbox' ? checked : value;
setValues(prev => ({ ...prev, [name]: fieldValue }));
if (touched[name] && validationSchema[name]) {
const error = validationSchema[name](fieldValue);
setErrors(prev => ({
...prev,
[name]: error
}));
}
}, [touched, validationSchema]);
const handleBlur = useCallback((e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
if (validationSchema[name]) {
const error = validationSchema[name](values[name]);
setErrors(prev => ({
...prev,
[name]: error
}));
}
}, [values, validationSchema]);
const validateForm = useCallback(() => {
const newErrors = {};
Object.keys(validationSchema).forEach(field => {
const error = validationSchema[field](values[field]);
if (error) {
newErrors[field] = error;
}
});
setErrors(newErrors);
setTouched(Object.keys(values).reduce((acc, field) => {
acc[field] = true;
return acc;
}, {}));
return Object.keys(newErrors).length === 0;
}, [values, validationSchema]);
const handleSubmit = useCallback(async (onSubmit) => {
if (validateForm()) {
setIsSubmitting(true);
try {
await onSubmit(values);
} catch (error) {
console.error('Form submission error:', error);
} finally {
setIsSubmitting(false);
}
}
}, [values, validateForm]);
const resetForm = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsSubmitting(false);
}, [initialValues]);
return {
values,
errors,
touched,
isSubmitting,
setValue,
handleChange,
handleBlur,
handleSubmit,
resetForm,
validateForm
};
}
// Usage example
function ContactForm() {
const validationSchema = {
name: (value) => {
if (!value.trim()) return 'Name is required';
if (value.length < 2) return 'Name must be at least 2 characters';
return '';
},
email: (value) => {
if (!value.trim()) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
return '';
},
message: (value) => {
if (!value.trim()) return 'Message is required';
if (value.length < 10) return 'Message must be at least 10 characters';
return '';
}
};
const {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
resetForm
} = useForm(
{ name: '', email: '', message: '' },
validationSchema
);
const onSubmit = async (formData) => {
console.log('Submitting form:', formData);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
alert('Form submitted successfully!');
resetForm();
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(onSubmit); }}>
<div>
<label>Name:</label>
<input
type="text"
name="name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
className={touched.name && errors.name ? 'error' : ''}
/>
{touched.name && errors.name && (
<span className="error-message">{errors.name}</span>
)}
</div>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
className={touched.email && errors.email ? 'error' : ''}
/>
{touched.email && errors.email && (
<span className="error-message">{errors.email}</span>
)}
</div>
<div>
<label>Message:</label>
<textarea
name="message"
value={values.message}
onChange={handleChange}
onBlur={handleBlur}
rows="4"
className={touched.message && errors.message ? 'error' : ''}
/>
{touched.message && errors.message && (
<span className="error-message">{errors.message}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}Form Libraries
While you can build forms with vanilla React, popular libraries can simplify complex form handling:
React Hook Form Example
import { useForm, Controller } from 'react-hook-form';
function ReactHookFormExample() {
const { control, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data) => {
console.log('Form data:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Name:</label>
<Controller
name="name"
control={control}
rules={{ required: 'Name is required' }}
render={({ field }) => (
<input
{...field}
type="text"
className={errors.name ? 'error' : ''}
/>
)}
/>
{errors.name && (
<span className="error-message">{errors.name.message}</span>
)}
</div>
<div>
<label>Email:</label>
<Controller
name="email"
control={control}
rules={{
required: 'Email is required',
pattern: {
value: /\S+@\S+\.\S+/,
message: 'Email is invalid'
}
}}
render={({ field }) => (
<input
{...field}
type="email"
className={errors.email ? 'error' : ''}
/>
)}
/>
{errors.email && (
<span className="error-message">{errors.email.message}</span>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}Best Practices
1. Accessibility
function AccessibleForm() {
const [email, setEmail] = useState('');
return (
<form>
<div>
<label htmlFor="email-input">
Email Address
<span className="required" aria-label="required">*</span>
</label>
<input
id="email-input"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
aria-describedby="email-help email-error"
aria-invalid={email && !/\S+@\S+\.\S+/.test(email)}
required
/>
<div id="email-help" className="help-text">
Enter your email address
</div>
{email && !/\S+@\S+\.\S+/.test(email) && (
<div id="email-error" className="error-message" role="alert">
Please enter a valid email address
</div>
)}
</div>
</form>
);
}2. Performance Optimization
function OptimizedForm() {
const [formData, setFormData] = useState({
field1: '',
field2: '',
field3: ''
});
// Use useCallback for event handlers
const handleChange = useCallback((e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
}, []);
// Memoize expensive computations
const computedValue = useMemo(() => {
return formData.field1.toUpperCase();
}, [formData.field1]);
return (
<form>
<input
name="field1"
value={formData.field1}
onChange={handleChange}
/>
<input
name="field2"
value={formData.field2}
onChange={handleChange}
/>
<input
name="field3"
value={formData.field3}
onChange={handleChange}
/>
<div>Computed: {computedValue}</div>
</form>
);
}Summary
Building forms in React involves several key concepts:
- Use controlled components for better form management
- Implement validation both on submit and in real-time
- Handle different input types appropriately
- Consider accessibility for all users
- Optimize performance with proper hooks and memoization
- Use form libraries for complex scenarios
- Create reusable form patterns and custom hooks
Mastering React forms will enable you to build sophisticated user interfaces with excellent user experience.
External Resources:
Related Tutorials: