Forms in React
React forms handle user input through component state. There are two main approaches: controlled components (React state controls input value) and uncontrolled components (DOM handles input state with refs).
Controlled Components
React JSXRead-only
1
import { useState } from 'react'; function ControlledForm() { const [formData, setFormData] = useState({ username: '', email: '', age: '', gender: '', interests: [], bio: '' }); const handleChange = (e) => { const { name, value, type, checked } = e.target; if (type === 'checkbox') { // Handle checkboxes if (checked) { setFormData({ ...formData, interests: [...formData.interests, value] }); } else { setFormData({ ...formData, interests: formData.interests.filter(item => item !== value) }); } } else { // Handle text inputs setFormData({ ...formData, [name]: value }); } }; const handleSubmit = (e) => { e.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="number" name="age" value={formData.age} onChange={handleChange} placeholder="Age" /> <select name="gender" value={formData.gender} onChange={handleChange}> <option value="">Select Gender</option> <option value="male">Male</option> <option value="female">Female</option> <option value="other">Other</option> </select> <label> <input type="checkbox" value="coding" checked={formData.interests.includes('coding')} onChange={handleChange} /> Coding </label> <textarea name="bio" value={formData.bio} onChange={handleChange} placeholder="Tell us about yourself" /> <button type="submit">Submit</button> </form> ); }
Uncontrolled Components
React JSXRead-only
1
import { useRef } from 'react'; function UncontrolledForm() { const usernameRef = useRef(null); const emailRef = useRef(null); const genderRef = useRef(null); const bioRef = useRef(null); const handleSubmit = (e) => { e.preventDefault(); const formData = { username: usernameRef.current.value, email: emailRef.current.value, gender: genderRef.current.value, bio: bioRef.current.value }; console.log('Form data:', formData); }; return ( <form onSubmit={handleSubmit}> <input type="text" ref={usernameRef} defaultValue="" placeholder="Username" /> <input type="email" ref={emailRef} defaultValue="" placeholder="Email" /> <select ref={genderRef} defaultValue=""> <option value="">Select Gender</option> <option value="male">Male</option> <option value="female">Female</option> </select> <textarea ref={bioRef} defaultValue="" placeholder="Bio" /> <button type="submit">Submit</button> </form> ); } // Using defaultValue for initial values function DefaultValueForm() { return ( <form> <input type="text" defaultValue="Initial value" /> <input type="checkbox" defaultChecked={true} /> <select defaultValue="male"> <option value="male">Male</option> <option value="female">Female</option> </select> </form> ); }
Form Validation
React JSXRead-only
1
import { useState } from 'react'; function ValidatedForm() { const [formData, setFormData] = useState({ username: '', email: '', password: '', confirmPassword: '' }); const [errors, setErrors] = useState({}); const [touched, setTouched] = 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'; return ''; case 'email': if (!value) return 'Email is required'; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) return 'Invalid email format'; return ''; case 'password': if (!value) return 'Password is required'; if (value.length < 6) return 'Password must be at least 6 characters'; if (!/[A-Z]/.test(value)) return 'Password must contain at least one uppercase letter'; if (!/[0-9]/.test(value)) return 'Password must contain at least one number'; return ''; case 'confirmPassword': if (!value) return 'Please confirm your password'; if (value !== formData.password) return 'Passwords do not match'; return ''; default: return ''; } }; const handleChange = (e) => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); // Validate on change if field has been touched if (touched[name]) { const error = validateField(name, value); setErrors({ ...errors, [name]: error }); } }; const handleBlur = (e) => { const { name, value } = e.target; setTouched({ ...touched, [name]: true }); const error = validateField(name, value); setErrors({ ...errors, [name]: error }); }; const handleSubmit = (e) => { e.preventDefault(); // Validate all fields const newErrors = {}; let isValid = true; Object.keys(formData).forEach(field => { const error = validateField(field, formData[field]); if (error) { newErrors[field] = error; isValid = false; } }); setErrors(newErrors); if (isValid) { console.log('Form submitted:', formData); alert('Form submitted successfully!'); } }; return ( <form onSubmit={handleSubmit}> <div> <label>Username:</label> <input type="text" name="username" value={formData.username} onChange={handleChange} onBlur={handleBlur} className={errors.username && touched.username ? 'error' : ''} /> {touched.username && errors.username && ( <span style={{ color: 'red' }}>{errors.username}</span> )} </div> <div> <label>Email:</label> <input type="email" name="email" value={formData.email} onChange={handleChange} onBlur={handleBlur} className={errors.email && touched.email ? 'error' : ''} /> {touched.email && errors.email && ( <span style={{ color: 'red' }}>{errors.email}</span> )} </div> <div> <label>Password:</label> <input type="password" name="password" value={formData.password} onChange={handleChange} onBlur={handleBlur} className={errors.password && touched.password ? 'error' : ''} /> {touched.password && errors.password && ( <span style={{ color: 'red' }}>{errors.password}</span> )} </div> <div> <label>Confirm Password:</label> <input type="password" name="confirmPassword" value={formData.confirmPassword} onChange={handleChange} onBlur={handleBlur} className={errors.confirmPassword && touched.confirmPassword ? 'error' : ''} /> {touched.confirmPassword && errors.confirmPassword && ( <span style={{ color: 'red' }}>{errors.confirmPassword}</span> )} </div> <button type="submit">Submit</button> </form> ); }
Custom Form Hook
React JSXRead-only
1
import { useState, useCallback } from 'react'; function useForm(initialValues, validate) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const handleChange = useCallback((e) => { const { name, value, type, checked } = e.target; const newValue = type === 'checkbox' ? checked : value; setValues(prev => ({ ...prev, [name]: newValue })); if (touched[name] && validate) { const error = validate(name, newValue, values); setErrors(prev => ({ ...prev, [name]: error })); } }, [touched, values, validate]); const handleBlur = useCallback((e) => { const { name, value } = e.target; setTouched(prev => ({ ...prev, [name]: true })); if (validate) { const error = validate(name, value, values); setErrors(prev => ({ ...prev, [name]: error })); } }, [values, validate]); const handleSubmit = useCallback((onSubmit) => async (e) => { e.preventDefault(); setIsSubmitting(true); // Validate all fields if (validate) { const newErrors = {}; let isValid = true; Object.keys(values).forEach(field => { const error = validate(field, values[field], values); if (error) { newErrors[field] = error; isValid = false; } }); setErrors(newErrors); if (isValid) { await onSubmit(values); } } else { await onSubmit(values); } setIsSubmitting(false); }, [values, validate]); const resetForm = useCallback(() => { setValues(initialValues); setErrors({}); setTouched({}); }, [initialValues]); return { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit, resetForm, setValues }; } // Usage function LoginForm() { const validate = (name, value, values) => { if (name === 'email') { if (!value) return 'Email is required'; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email'; } if (name === 'password') { if (!value) return 'Password is required'; if (value.length < 6) return 'Password must be at least 6 characters'; } return ''; }; const { values, errors, touched, handleChange, handleBlur, handleSubmit } = useForm({ email: '', password: '' }, validate); const onSubmit = async (data) => { console.log('Login data:', data); // API call here }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <input name="email" value={values.email} onChange={handleChange} onBlur={handleBlur} placeholder="Email" /> {touched.email && errors.email && <span>{errors.email}</span>} </div> <div> <input name="password" type="password" value={values.password} onChange={handleChange} onBlur={handleBlur} placeholder="Password" /> {touched.password && errors.password && <span>{errors.password}</span>} </div> <button type="submit">Login</button> </form> ); }
React Hook Form (Recommended Library)
React JSXRead-only
1
import { useForm } from 'react-hook-form'; function ReactHookFormExample() { const { register, handleSubmit, formState: { errors, isSubmitting }, reset } = useForm({ defaultValues: { username: '', email: '', age: 18 } }); const onSubmit = async (data) => { console.log(data); await new Promise(resolve => setTimeout(resolve, 1000)); reset(); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <div> <input {...register('username', { required: 'Username is required', minLength: { value: 3, message: 'Username must be at least 3 characters' } })} placeholder="Username" /> {errors.username && ( <span>{errors.username.message}</span> )} </div> <div> <input {...register('email', { required: 'Email is required', pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Invalid email format' } })} placeholder="Email" /> {errors.email && <span>{errors.email.message}</span>} </div> <div> <input type="number" {...register('age', { min: { value: 18, message: 'Must be at least 18' }, max: { value: 100, message: 'Must be at most 100' } })} placeholder="Age" /> {errors.age && <span>{errors.age.message}</span>} </div> <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Submitting...' : 'Submit'} </button> </form> ); }
Form Best Practices
- Use controlled components for most forms (easier validation and state management)
- Implement validation on both client and server side
- Show validation errors inline, after field blur or on submit
- Disable submit button during submission to prevent duplicates
- Provide clear error messages that explain how to fix the issue
- Use form libraries like React Hook Form or Formik for complex forms
- Handle accessibility with proper labels and ARIA attributes
- Reset form after successful submission to clear inputs
- Save form state to localStorage for long forms
- Use TypeScript for better type safety with form data
Form Libraries Comparison
| Library | Bundle Size | Performance | Features | Learning Curve |
|---|---|---|---|---|
| React Hook Form | ~9KB | Excellent | Uncontrolled by default, minimal re-renders | Low |
| Formik | ~12KB | Good | Full-featured, controlled | Low |
| React Final Form | ~8KB | Good | Subscription-based, performant | Medium |
| React Forms (native) | 0KB | Good | Basic, requires custom code | Low |
Common Mistakes
React JSXRead-only
1
// ❌ Wrong: Not handling form submission <form> <input /> <button type="submit">Submit</button> </form> // ✅ Correct: Handle submit and prevent default <form onSubmit={handleSubmit}> <input /> <button type="submit">Submit</button> </form> // ❌ Wrong: Not providing name attributes <input value={username} onChange={handleChange} /> // ✅ Correct: Include name attribute <input name="username" value={username} onChange={handleChange} /> // ❌ Wrong: Mutating state directly const handleChange = (e) => { formData[e.target.name] = e.target.value; // Direct mutation setFormData(formData); }; // ✅ Correct: Create new object const handleChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; // ❌ Wrong: Not resetting form after submit const handleSubmit = async (data) => { await submitData(data); // Form still has old data }; // ✅ Correct: Reset after submission const handleSubmit = async (data) => { await submitData(data); resetForm(); };
Conclusion
Forms are a critical part of most React applications. Controlled components give you full control over form state and validation. For complex forms, consider using libraries like React Hook Form for better performance and developer experience. Always implement proper validation, error handling, and accessibility.