Form Validation
Form Validation in React
Form validation is crucial for ensuring data integrity and providing a good user experience. React offers various approaches to implement form validation effectively.
Basic Validation
Simple Required Field Validation
function SimpleForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!email) {
setError('Email is required');
return;
}
if (!email.includes('@')) {
setError('Please enter a valid email');
return;
}
console.log('Form submitted:', email);
setError('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
setError(''); // Clear error on change
}}
placeholder="Enter email"
/>
{error && <span style={{ color: 'red' }}>{error}</span>}
<button type="submit">Submit</button>
</form>
);
}
Real-time Validation
Validate on Change
function RealtimeValidation() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const validateField = (name, value) => {
switch (name) {
case 'username':
if (!value) return 'Username is required';
if (value.length < 3) return 'Username must be at least 3 characters';
if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Username can only contain letters, numbers, and underscores';
break;
case 'email':
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';
break;
case 'password':
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
if (!/(?=.*[0-9])/.test(value)) return 'Password must contain at least one number';
if (!/(?=.*[a-z])/.test(value)) return 'Password must contain at least one lowercase letter';
if (!/(?=.*[A-Z])/.test(value)) return 'Password must contain at least one uppercase letter';
break;
default:
return '';
}
return '';
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Validate field in real-time
const error = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
};
return (
<form>
<div>
<input
name="username"
value={formData.username}
onChange={handleChange}
placeholder="Username"
/>
{errors.username && <span className="error">{errors.username}</span>}
</div>
<div>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
</form>
);
}
Validation on Blur
function BlurValidation() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
age: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validateField = (name, value) => {
switch (name) {
case 'firstName':
case 'lastName':
if (!value) return `${name === 'firstName' ? 'First' : 'Last'} name is required`;
if (value.length < 2) return 'Must be at least 2 characters';
if (!/^[a-zA-Z\s-']+$/.test(value)) return 'Only letters, spaces, hyphens, and apostrophes allowed';
break;
case 'age':
if (!value) return 'Age is required';
const ageNum = parseInt(value);
if (isNaN(ageNum)) return 'Age must be a number';
if (ageNum < 0 || ageNum > 150) return 'Age must be between 0 and 150';
break;
}
return '';
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
const error = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
};
return (
<form>
<div>
<input
name="firstName"
value={formData.firstName}
onChange={handleChange}
onBlur={handleBlur}
placeholder="First Name"
/>
{touched.firstName && errors.firstName && (
<span className="error">{errors.firstName}</span>
)}
</div>
<div>
<input
name="lastName"
value={formData.lastName}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Last Name"
/>
{touched.lastName && errors.lastName && (
<span className="error">{errors.lastName}</span>
)}
</div>
<div>
<input
name="age"
type="number"
value={formData.age}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Age"
/>
{touched.age && errors.age && (
<span className="error">{errors.age}</span>
)}
</div>
</form>
);
}
Custom Validation Hook
function useValidation(initialValues, validationRules) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validate = (fieldName = null) => {
const fieldsToValidate = fieldName ? [fieldName] : Object.keys(validationRules);
const newErrors = { ...errors };
fieldsToValidate.forEach(field => {
const rules = validationRules[field];
if (!rules) return;
const value = values[field];
let error = '';
for (const rule of rules) {
const result = rule(value, values);
if (result !== true) {
error = result;
break;
}
}
if (error) {
newErrors[field] = error;
} else {
delete newErrors[field];
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
const newValue = type === 'checkbox' ? checked : value;
setValues(prev => ({ ...prev, [name]: newValue }));
if (touched[name]) {
validate(name);
}
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
validate(name);
};
const handleSubmit = (callback) => (e) => {
e.preventDefault();
// Touch all fields
const allTouched = Object.keys(values).reduce(
(acc, key) => ({ ...acc, [key]: true }),
{}
);
setTouched(allTouched);
if (validate()) {
callback(values);
}
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
validate,
setValues
};
}
// Validation rules
const required = (message = 'This field is required') =>
value => value ? true : message;
const minLength = (min, message) =>
value => !value || value.length >= min ? true : message || `Must be at least ${min} characters`;
const maxLength = (max, message) =>
value => !value || value.length <= max ? true : message || `Must be at most ${max} characters`;
const pattern = (regex, message) =>
value => !value || regex.test(value) ? true : message;
const email = (message = 'Invalid email address') =>
pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, message);
const matchField = (fieldName, message) =>
(value, allValues) => value === allValues[fieldName] ? true : message;
// Usage example
function RegistrationForm() {
const validationRules = {
username: [
required(),
minLength(3, 'Username must be at least 3 characters'),
pattern(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores allowed')
],
email: [
required(),
email()
],
password: [
required(),
minLength(8),
pattern(/[0-9]/, 'Must contain at least one number'),
pattern(/[a-z]/, 'Must contain at least one lowercase letter'),
pattern(/[A-Z]/, 'Must contain at least one uppercase letter')
],
confirmPassword: [
required(),
matchField('password', 'Passwords do not match')
]
};
const {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit
} = useValidation(
{ username: '', email: '', password: '', confirmPassword: '' },
validationRules
);
const onSubmit = (data) => {
console.log('Form submitted:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
name="username"
value={values.username}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Username"
/>
{touched.username && errors.username && (
<span className="error">{errors.username}</span>
)}
</div>
<div>
<input
name="email"
type="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Email"
/>
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<div>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Password"
/>
{touched.password && errors.password && (
<span className="error">{errors.password}</span>
)}
</div>
<div>
<input
name="confirmPassword"
type="password"
value={values.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Confirm Password"
/>
{touched.confirmPassword && errors.confirmPassword && (
<span className="error">{errors.confirmPassword}</span>
)}
</div>
<button type="submit">Register</button>
</form>
);
}
Async Validation
function AsyncValidationForm() {
const [username, setUsername] = useState('');
const [isChecking, setIsChecking] = useState(false);
const [isAvailable, setIsAvailable] = useState(null);
const [error, setError] = useState('');
// Simulate API call to check username availability
const checkUsernameAvailability = async (username) => {
setIsChecking(true);
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Simulate some usernames being taken
const takenUsernames = ['admin', 'user', 'test'];
const available = !takenUsernames.includes(username.toLowerCase());
setIsAvailable(available);
setIsChecking(false);
return available;
};
useEffect(() => {
if (!username) {
setError('');
setIsAvailable(null);
return;
}
if (username.length < 3) {
setError('Username must be at least 3 characters');
setIsAvailable(null);
return;
}
setError('');
// Debounce the API call
const timer = setTimeout(() => {
checkUsernameAvailability(username);
}, 500);
return () => clearTimeout(timer);
}, [username]);
return (
<form>
<div>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Choose a username"
/>
{isChecking && <span>Checking availability...</span>}
{error && <span className="error">{error}</span>}
{isAvailable === true && <span className="success">ā Available</span>}
{isAvailable === false && <span className="error">ā Username taken</span>}
</div>
</form>
);
}
Complex Form Validation
function ComplexValidationForm() {
const [formData, setFormData] = useState({
personalInfo: {
firstName: '',
lastName: '',
dateOfBirth: ''
},
contactInfo: {
email: '',
phone: '',
address: ''
},
preferences: {
newsletter: false,
notifications: {
email: false,
sms: false,
push: false
}
}
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validatePersonalInfo = (data) => {
const errors = {};
if (!data.firstName) {
errors.firstName = 'First name is required';
}
if (!data.lastName) {
errors.lastName = 'Last name is required';
}
if (!data.dateOfBirth) {
errors.dateOfBirth = 'Date of birth is required';
} else {
const age = new Date().getFullYear() - new Date(data.dateOfBirth).getFullYear();
if (age < 18) {
errors.dateOfBirth = 'You must be at least 18 years old';
}
}
return errors;
};
const validateContactInfo = (data) => {
const errors = {};
if (!data.email) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.email = 'Invalid email format';
}
if (!data.phone) {
errors.phone = 'Phone is required';
} else if (!/^\+?[\d\s-()]+$/.test(data.phone)) {
errors.phone = 'Invalid phone format';
}
if (!data.address) {
errors.address = 'Address is required';
}
return errors;
};
const validatePreferences = (data) => {
const errors = {};
if (data.newsletter && !Object.values(data.notifications).some(v => v)) {
errors.notifications = 'Select at least one notification method for newsletter';
}
return errors;
};
const validateForm = () => {
const newErrors = {
personalInfo: validatePersonalInfo(formData.personalInfo),
contactInfo: validateContactInfo(formData.contactInfo),
preferences: validatePreferences(formData.preferences)
};
setErrors(newErrors);
// Check if there are any errors
const hasErrors = Object.values(newErrors).some(section =>
Object.keys(section).length > 0
);
return !hasErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('Form is valid:', formData);
// Submit form
} else {
console.log('Form has errors');
}
};
const updateNestedState = (section, field, value) => {
setFormData(prev => ({
...prev,
[section]: {
...prev[section],
[field]: value
}
}));
};
return (
<form onSubmit={handleSubmit}>
<fieldset>
<legend>Personal Information</legend>
<input
value={formData.personalInfo.firstName}
onChange={(e) => updateNestedState('personalInfo', 'firstName', e.target.value)}
placeholder="First Name"
/>
{errors.personalInfo?.firstName && (
<span className="error">{errors.personalInfo.firstName}</span>
)}
<input
value={formData.personalInfo.lastName}
onChange={(e) => updateNestedState('personalInfo', 'lastName', e.target.value)}
placeholder="Last Name"
/>
{errors.personalInfo?.lastName && (
<span className="error">{errors.personalInfo.lastName}</span>
)}
<input
type="date"
value={formData.personalInfo.dateOfBirth}
onChange={(e) => updateNestedState('personalInfo', 'dateOfBirth', e.target.value)}
/>
{errors.personalInfo?.dateOfBirth && (
<span className="error">{errors.personalInfo.dateOfBirth}</span>
)}
</fieldset>
<fieldset>
<legend>Contact Information</legend>
<input
type="email"
value={formData.contactInfo.email}
onChange={(e) => updateNestedState('contactInfo', 'email', e.target.value)}
placeholder="Email"
/>
{errors.contactInfo?.email && (
<span className="error">{errors.contactInfo.email}</span>
)}
<input
type="tel"
value={formData.contactInfo.phone}
onChange={(e) => updateNestedState('contactInfo', 'phone', e.target.value)}
placeholder="Phone"
/>
{errors.contactInfo?.phone && (
<span className="error">{errors.contactInfo.phone}</span>
)}
<textarea
value={formData.contactInfo.address}
onChange={(e) => updateNestedState('contactInfo', 'address', e.target.value)}
placeholder="Address"
/>
{errors.contactInfo?.address && (
<span className="error">{errors.contactInfo.address}</span>
)}
</fieldset>
<fieldset>
<legend>Preferences</legend>
<label>
<input
type="checkbox"
checked={formData.preferences.newsletter}
onChange={(e) => updateNestedState('preferences', 'newsletter', e.target.checked)}
/>
Subscribe to newsletter
</label>
{formData.preferences.newsletter && (
<div>
<p>Notification methods:</p>
<label>
<input
type="checkbox"
checked={formData.preferences.notifications.email}
onChange={(e) => setFormData(prev => ({
...prev,
preferences: {
...prev.preferences,
notifications: {
...prev.preferences.notifications,
email: e.target.checked
}
}
}))}
/>
Email
</label>
<label>
<input
type="checkbox"
checked={formData.preferences.notifications.sms}
onChange={(e) => setFormData(prev => ({
...prev,
preferences: {
...prev.preferences,
notifications: {
...prev.preferences.notifications,
sms: e.target.checked
}
}
}))}
/>
SMS
</label>
<label>
<input
type="checkbox"
checked={formData.preferences.notifications.push}
onChange={(e) => setFormData(prev => ({
...prev,
preferences: {
...prev.preferences,
notifications: {
...prev.preferences.notifications,
push: e.target.checked
}
}
}))}
/>
Push Notifications
</label>
</div>
)}
{errors.preferences?.notifications && (
<span className="error">{errors.preferences.notifications}</span>
)}
</fieldset>
<button type="submit">Submit</button>
</form>
);
}
Validation Schema with Yup-like Implementation
// Simple schema validation implementation
class Schema {
constructor(shape) {
this.shape = shape;
}
validate(data) {
const errors = {};
Object.keys(this.shape).forEach(key => {
const validator = this.shape[key];
const result = validator.validate(data[key]);
if (result) {
errors[key] = result;
}
});
return Object.keys(errors).length > 0 ? errors : null;
}
}
class StringValidator {
constructor() {
this.checks = [];
}
required(message = 'This field is required') {
this.checks.push(value => !value ? message : null);
return this;
}
min(length, message) {
this.checks.push(value =>
value && value.length < length
? message || `Must be at least ${length} characters`
: null
);
return this;
}
max(length, message) {
this.checks.push(value =>
value && value.length > length
? message || `Must be at most ${length} characters`
: null
);
return this;
}
email(message = 'Invalid email') {
this.checks.push(value =>
value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
? message
: null
);
return this;
}
validate(value) {
for (const check of this.checks) {
const error = check(value);
if (error) return error;
}
return null;
}
}
const string = () => new StringValidator();
// Usage
function SchemaValidationForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
bio: ''
});
const [errors, setErrors] = useState({});
const schema = new Schema({
name: string().required().min(2).max(50),
email: string().required().email(),
bio: string().max(500, 'Bio is too long')
});
const handleSubmit = (e) => {
e.preventDefault();
const validationErrors = schema.validate(formData);
if (validationErrors) {
setErrors(validationErrors);
} else {
setErrors({});
console.log('Valid data:', formData);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear error for this field
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: null }));
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div>
<input
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<textarea
name="bio"
value={formData.bio}
onChange={handleChange}
placeholder="Bio (optional)"
/>
{errors.bio && <span className="error">{errors.bio}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
}
Best Practices
- Validate on the right events - Usually onBlur for individual fields
- Show errors at the right time - Not while user is typing
- Clear error messages - Use descriptive, actionable messages
- Client and server validation - Never trust client-side validation alone
- Accessibility - Use ARIA attributes for screen readers
- Visual feedback - Use colors and icons to indicate validation state
- Progressive enhancement - Form should work without JavaScript
- Consistent validation - Same rules on client and server
Form validation is essential for data quality and user experience. Implement it thoughtfully to guide users without frustrating them!