Forms without validation are like doors without locks - anyone can submit anything! Validation ensures users provide the right data in the right format before submission. You'll learn how to validate forms in React, when to show errors, how to display helpful messages, and patterns for building forms with great user experience. Let's make your forms bulletproof! 🛡️
Validation Basics
Form validation checks if user input meets your requirements:
Basic Required Field Validation
function Form() {
const [email, setEmail] = React.useState('')
const [error, setError] = React.useState('')
const handleSubmit = (e) => {
e.preventDefault()
// Validate on submit
if (!email) {
setError('Email is required')
return
}
if (!email.includes('@')) {
setError('Email must be valid')
return
}
// Clear error if valid
setError('')
console.log('Submitting:', email)
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">Submit</button>
</form>
)
}
// Validates only when user tries to submitCommon Validation Types:
- Required - Field must not be empty
- Format - Email, phone, URL patterns
- Length - Min/max character count
- Range - Min/max numeric values
- Custom - Password strength, matching fields
When to Validate
1. On Submit (Basic)
function Form() {
const [data, setData] = React.useState({ name: '', email: '' })
const [errors, setErrors] = React.useState({})
const handleSubmit = (e) => {
e.preventDefault()
const newErrors = {}
if (!data.name) {
newErrors.name = 'Name is required'
}
if (!data.email) {
newErrors.email = 'Email is required'
} else if (!data.email.includes('@')) {
newErrors.email = 'Email must be valid'
}
setErrors(newErrors)
// Only submit if no errors
if (Object.keys(newErrors).length === 0) {
console.log('Form is valid, submitting...')
}
}
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={data.name}
onChange={(e) => setData({ ...data, name: e.target.value })}
/>
{errors.name && <p className="error">{errors.name}</p>}
<input
name="email"
value={data.email}
onChange={(e) => setData({ ...data, email: e.target.value })}
/>
{errors.email && <p className="error">{errors.email}</p>}
<button type="submit">Submit</button>
</form>
)
}
// Pros: Simple, doesn't annoy users while typing
// Cons: User only sees errors after submit attempt2. On Blur (Better UX)
function Form() {
const [email, setEmail] = React.useState('')
const [error, setError] = React.useState('')
const [touched, setTouched] = React.useState(false)
const validateEmail = (value) => {
if (!value) return 'Email is required'
if (!value.includes('@')) return 'Email must be valid'
return ''
}
const handleBlur = () => {
setTouched(true)
setError(validateEmail(email))
}
const handleChange = (e) => {
setEmail(e.target.value)
// Only validate if field was already touched
if (touched) {
setError(validateEmail(e.target.value))
}
}
return (
<div>
<input
type="email"
value={email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Email"
/>
{touched && error && <p className="error">{error}</p>}
</div>
)
}
// Pros: Better UX - validates when user leaves field
// Cons: Slightly more complex3. Real-time (Most Responsive)
function Form() {
const [password, setPassword] = React.useState('')
const [errors, setErrors] = React.useState([])
const validatePassword = (value) => {
const newErrors = []
if (value.length < 8) {
newErrors.push('At least 8 characters')
}
if (!/[A-Z]/.test(value)) {
newErrors.push('At least one uppercase letter')
}
if (!/[0-9]/.test(value)) {
newErrors.push('At least one number')
}
return newErrors
}
const handleChange = (e) => {
const value = e.target.value
setPassword(value)
setErrors(validatePassword(value))
}
return (
<div>
<input
type="password"
value={password}
onChange={handleChange}
placeholder="Password"
/>
{errors.length > 0 && (
<ul className="error-list">
{errors.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
)}
{password && errors.length === 0 && (
<p className="success">Password is strong!</p>
)}
</div>
)
}
// Pros: Immediate feedback, great for password strength
// Cons: Can be annoying for some fields⏰ Validation Timing Best Practices
Use on submit for: Overall form validation
Use on blur for: Most text fields (email, name, etc.)
Use real-time for: Password strength, username availability
Combine approaches: Real-time for some fields, blur for others!
Common Validation Patterns
Required Field
const validateRequired = (value, fieldName) => {
if (!value || value.trim() === '') {
return `${fieldName} is required`
}
return ''
}
// Usage
const nameError = validateRequired(name, 'Name')
const emailError = validateRequired(email, 'Email')Email Format
const validateEmail = (email) => {
if (!email) {
return 'Email is required'
}
// Simple email regex
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
return 'Please enter a valid email'
}
return ''
}
// More comprehensive version
const validateEmailAdvanced = (email) => {
if (!email) return 'Email is required'
if (email.length > 254) {
return 'Email is too long'
}
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
if (!emailRegex.test(email)) {
return 'Please enter a valid email address'
}
return ''
}Password Strength
const validatePassword = (password) => {
const errors = []
if (password.length < 8) {
errors.push('Must be at least 8 characters')
}
if (!/[a-z]/.test(password)) {
errors.push('Must contain a lowercase letter')
}
if (!/[A-Z]/.test(password)) {
errors.push('Must contain an uppercase letter')
}
if (!/[0-9]/.test(password)) {
errors.push('Must contain a number')
}
if (!/[!@#$%^&*]/.test(password)) {
errors.push('Must contain a special character (!@#$%^&*)')
}
return errors
}
// Returns array of errors (empty if valid)Matching Fields
const validatePasswordMatch = (password, confirmPassword) => {
if (!confirmPassword) {
return 'Please confirm your password'
}
if (password !== confirmPassword) {
return 'Passwords do not match'
}
return ''
}
// Usage in component
function PasswordForm() {
const [password, setPassword] = React.useState('')
const [confirmPassword, setConfirmPassword] = React.useState('')
const [error, setError] = React.useState('')
const handleConfirmBlur = () => {
setError(validatePasswordMatch(password, confirmPassword))
}
return (
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
onBlur={handleConfirmBlur}
placeholder="Confirm Password"
/>
{error && <p className="error">{error}</p>}
</div>
)
}Length Validation
const validateLength = (value, min, max, fieldName) => {
if (value.length < min) {
return `${fieldName} must be at least ${min} characters`
}
if (value.length > max) {
return `${fieldName} must be less than ${max} characters`
}
return ''
}
// Usage
const usernameError = validateLength(username, 3, 20, 'Username')
const bioError = validateLength(bio, 0, 500, 'Bio')Number Range
const validateRange = (value, min, max, fieldName) => {
const num = Number(value)
if (isNaN(num)) {
return `${fieldName} must be a number`
}
if (num < min) {
return `${fieldName} must be at least ${min}`
}
if (num > max) {
return `${fieldName} must be at most ${max}`
}
return ''
}
// Usage
const ageError = validateRange(age, 0, 120, 'Age')
const quantityError = validateRange(quantity, 1, 100, 'Quantity')Complete Validation Example
function RegistrationForm() {
const [formData, setFormData] = React.useState({
username: '',
email: '',
password: '',
confirmPassword: '',
age: ''
})
const [errors, setErrors] = React.useState({})
const [touched, setTouched] = React.useState({})
const [isSubmitting, setIsSubmitting] = React.useState(false)
// Validation functions
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 (value.length > 20) return 'Username must be less than 20 characters'
return ''
case 'email':
if (!value) return 'Email is required'
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Please enter a valid email'
}
return ''
case 'password':
if (!value) return 'Password is required'
if (value.length < 8) return 'Password must be at least 8 characters'
if (!/[A-Z]/.test(value)) return 'Password must contain an uppercase letter'
if (!/[0-9]/.test(value)) return 'Password must contain a number'
return ''
case 'confirmPassword':
if (!value) return 'Please confirm your password'
if (value !== formData.password) return 'Passwords do not match'
return ''
case 'age':
if (!value) return 'Age is required'
const age = Number(value)
if (isNaN(age)) return 'Age must be a number'
if (age < 13) return 'You must be at least 13 years old'
if (age > 120) return 'Please enter a valid age'
return ''
default:
return ''
}
}
const handleChange = (e) => {
const { name, value } = e.target
setFormData({ ...formData, [name]: value })
// Validate if field was touched
if (touched[name]) {
setErrors({ ...errors, [name]: validateField(name, value) })
}
}
const handleBlur = (e) => {
const { name, value } = e.target
setTouched({ ...touched, [name]: true })
setErrors({ ...errors, [name]: validateField(name, value) })
}
const handleSubmit = async (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)
// Stop if there are errors
if (Object.keys(newErrors).length > 0) {
return
}
// Submit form
setIsSubmitting(true)
try {
await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
console.log('Registration successful!')
} catch (error) {
console.error('Registration failed:', error)
} finally {
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="username"
value={formData.username}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Username"
/>
{touched.username && errors.username && (
<p className="error">{errors.username}</p>
)}
</div>
<div>
<input
name="email"
type="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Email"
/>
{touched.email && errors.email && (
<p className="error">{errors.email}</p>
)}
</div>
<div>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Password"
/>
{touched.password && errors.password && (
<p className="error">{errors.password}</p>
)}
</div>
<div>
<input
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Confirm Password"
/>
{touched.confirmPassword && errors.confirmPassword && (
<p className="error">{errors.confirmPassword}</p>
)}
</div>
<div>
<input
name="age"
type="number"
value={formData.age}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Age"
/>
{touched.age && errors.age && (
<p className="error">{errors.age}</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Registering...' : 'Register'}
</button>
</form>
)
}Interactive Demo
💡 Tip: Edit the code above and click "Run" to see your changes
Displaying Error Messages
Inline Errors
function Field({ label, error, ...inputProps }) {
return (
<div className="field">
<label>{label}</label>
<input
{...inputProps}
className={error ? 'input-error' : 'input'}
/>
{error && <span className="error-message">{error}</span>}
</div>
)
}
// CSS
.input-error {
border-color: red;
}
.error-message {
color: red;
font-size: 12px;
margin-top: 4px;
}Summary Errors
function Form() {
const [errors, setErrors] = React.useState({})
const errorMessages = Object.values(errors).filter(Boolean)
return (
<form>
{errorMessages.length > 0 && (
<div className="error-summary">
<h3>Please fix the following errors:</h3>
<ul>
{errorMessages.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
{/* Form fields */}
</form>
)
}Success Messages
function Field({ value, error, touched, validMessage }) {
const isValid = touched && !error && value
return (
<div>
<input value={value} />
{error && touched && (
<span className="error">❌ {error}</span>
)}
{isValid && (
<span className="success">✓ {validMessage}</span>
)}
</div>
)
}Validation Best Practices
- Validate on blur for most fields - Don't annoy users while typing
- Use real-time for password strength - Immediate feedback helps
- Always validate on submit - Final check before submission
- Show errors after user interaction - Use touched state
- Be specific in error messages - "Password must contain a number" not "Invalid password"
- Disable submit during validation - Prevent double submission
- Clear errors on successful submit - Reset form state
- Use consistent styling - Red for errors, green for success
- Validate on the server too - Never trust client-side only!
- Consider accessibility - Use aria attributes for screen readers
Client-Side Validation is NOT Enough!
Client-side validation improves user experience, but it can be bypassed. Always validate on the server too! Users can:
• Disable JavaScript
• Modify client-side code
• Send requests directly to your API
Client-side = UX. Server-side = Security.
Key Takeaways
- Validation ensures users provide correct data
- Validate on submit for basic validation
- Validate on blur for better user experience
- Use real-time validation for password strength
- Store errors in state (object for multiple fields)
- Track "touched" state to avoid showing errors too early
- Common validations: required, email, password, length, range
- Display errors inline near the relevant field
- Be specific and helpful in error messages
- Show success indicators for valid fields
- Disable submit button during submission
- Always validate on the server too!
- Validation improves data quality and user experience
What's Next?
Congratulations! You've completed the Events and Interactivity section. You now know:
- How to handle user interactions with events
- How to work with event objects and prevent defaults
- How to build controlled forms in React
- How to validate forms and display helpful errors
You have all the tools to build fully interactive, user-friendly forms and applications! But there's still so much more to learn:
- Side Effects - API calls, timers, and more
- Custom Hooks - Reusable logic patterns
- Context API - Sharing state across components
- Performance - Optimization techniques
- And much more!
Keep building - you're becoming a React expert! 🎉