Forms and Input Handling
Build complex forms with validation and error handling in React.
Understanding Forms in React: From Basic to Advanced
Forms are everywhere on the web - from simple search boxes to complex registration forms. In React, we handle forms differently than in traditional HTML, giving us more control and better user experiences. Let's start from the very basics and build up to advanced form handling techniques.
Why Forms Matter Forms are how users interact with your application:
- Login forms: How users access their accounts
- Registration forms: How new users sign up
- Contact forms: How users get in touch
- Order forms: How users make purchases
- Settings forms: How users customize their experience
The Traditional HTML Way vs The React Way In traditional HTML, forms work like this:
- User fills out the form
- User clicks submit
- Browser sends data to server
- Page refreshes with response
In React, we can do much better:
- User types → React updates immediately
- Instant validation as they type
- Dynamic form fields based on choices
- Submit without page refresh
- Better error handling and user feedback
Your First React Form: Starting Simple Before diving into controlled components, let's see the simplest possible form in React. This will help you understand why controlled components are useful.
1// 🎯 SIMPLE FORM EXAMPLE - Understanding the Basics23import React from 'react';45// ❌ NOT RECOMMENDED: Basic form without React state6function SimpleHTMLForm() {7 const handleSubmit = (e) => {8 e.preventDefault(); // Prevent page refresh910 // Access form data the old-fashioned way11 const formData = new FormData(e.target);12 const name = formData.get('name');13 const email = formData.get('email');1415 console.log('Submitted:', { name, email });16 };1718 return (19 <form onSubmit={handleSubmit}>20 <h3>Simple HTML Form (Not Recommended)</h3>2122 <div>23 <label>Name:</label>24 <input type="text" name="name" />25 </div>2627 <div>28 <label>Email:</label>29 <input type="email" name="email" />30 </div>3132 <button type="submit">Submit</button>33 </form>34 );35}3637// ✅ RECOMMENDED: Your first controlled form38function MyFirstControlledForm() {39 // Step 1: Create state for each input40 const [name, setName] = useState('');41 const [email, setEmail] = useState('');4243 // Step 2: Handle input changes44 const handleNameChange = (e) => {45 setName(e.target.value);46 };4748 const handleEmailChange = (e) => {49 setEmail(e.target.value);50 };5152 // Step 3: Handle form submission53 const handleSubmit = (e) => {54 e.preventDefault();55 console.log('Submitted:', { name, email });5657 // Clear form after submission58 setName('');59 setEmail('');60 };6162 return (63 <form onSubmit={handleSubmit}>64 <h3>My First Controlled Form</h3>6566 <div>67 <label>68 Name:69 <input70 type="text"71 value={name} // Controlled by React state72 onChange={handleNameChange} // Update state on change73 />74 </label>75 <p>You typed: {name}</p> {/* See the value in real-time! */}76 </div>7778 <div>79 <label>80 Email:81 <input82 type="email"83 value={email}84 onChange={handleEmailChange}85 />86 </label>87 <p>Your email: {email}</p>88 </div>8990 <button type="submit">Submit</button>9192 {/* Show current form state */}93 <div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f0f0f0' }}>94 <h4>Current Form State:</h4>95 <pre>{JSON.stringify({ name, email }, null, 2)}</pre>96 </div>97 </form>98 );99}
Controlled Components: The Foundation of React Forms
Controlled components are one of the most important concepts in React form handling. Think of them as form inputs that are "controlled" by React state - React becomes the single source of truth for what the user sees and types. This might seem like extra work at first, but it gives you superpowers when building interactive forms!
What Are Controlled Components?
In traditional HTML, form elements like <input>
, <textarea>
, and <select>
maintain their own state. When a user types into an input field, the browser manages what's displayed. With controlled components, React takes over this responsibility. The current value of the input is stored in React state, and any changes go through React first.
Real-World Analogy Imagine you're a teacher writing on a whiteboard. In traditional HTML forms, it's like students writing directly on the board themselves - you can see what they wrote, but you don't control it. With controlled components, it's like students telling you what to write, and you write it on the board. You have complete control over what appears, how it's formatted, and whether to accept or reject their input.
Why Use Controlled Components?
- Single Source of Truth: All form data lives in one place (React state), making it easy to access and manage
- Input Validation: You can validate user input in real-time as they type
- Input Formatting: Automatically format phone numbers, credit cards, or dates as users type
- Conditional Logic: Show/hide fields or change options based on other inputs
- Form Persistence: Easily save form state to localStorage or restore previous values
- Debugging: See exactly what's in your form at any time through React DevTools
The Two-Way Data Binding Pattern Controlled components implement a pattern called "two-way data binding":
- The component's state determines what's displayed in the input (state → UI)
- User interactions update the state, which then updates the display (UI → state → UI)
Common Input Types and How to Control Them:
- Text Inputs: Control with
value
prop - Checkboxes: Control with
checked
prop - Radio Buttons: Control with
checked
prop for each option - Select Dropdowns: Control with
value
prop on the select element - Textareas: Control with
value
prop
Benefits Over Uncontrolled Components:
- Instant input validation
- Dynamic form fields
- Easier testing
- Better integration with React ecosystem
- Predictable behavior
1// 🌟 CONTROLLED COMPONENTS EXAMPLE2// Demonstrating basic form input types and patterns34import React, { useState } from 'react';56function BasicControlledForm() {7 // Single source of truth for all form data8 const [formData, setFormData] = useState({9 username: '',10 email: '',11 age: '',12 bio: '',13 country: '',14 newsletter: false,15 experience: 'beginner',16 interests: []17 });1819 // Generic handler for most inputs20 const handleInputChange = (e) => {21 const { name, value, type, checked } = e.target;2223 setFormData(prev => ({24 ...prev,25 [name]: type === 'checkbox' ? checked : value26 }));27 };2829 // Special handler for multi-select checkboxes30 const handleInterestChange = (interest) => {31 setFormData(prev => ({32 ...prev,33 interests: prev.interests.includes(interest)34 ? prev.interests.filter(i => i !== interest)35 : [...prev.interests, interest]36 }));37 };3839 const handleSubmit = (e) => {40 e.preventDefault();41 console.log('Form submitted:', formData);42 };4344 return (45 <form onSubmit={handleSubmit}>46 {/* Text Input */}47 <div>48 <label>49 Username:50 <input51 type="text"52 name="username"53 value={formData.username}54 onChange={handleInputChange}55 />56 </label>57 </div>5859 {/* Email Input */}60 <div>61 <label>62 Email:63 <input64 type="email"65 name="email"66 value={formData.email}67 onChange={handleInputChange}68 />69 </label>70 </div>7172 {/* Number Input */}73 <div>74 <label>75 Age:76 <input77 type="number"78 name="age"79 value={formData.age}80 onChange={handleInputChange}81 min="1"82 max="120"83 />84 </label>85 </div>8687 {/* Textarea */}88 <div>89 <label>90 Bio:91 <textarea92 name="bio"93 value={formData.bio}94 onChange={handleInputChange}95 rows="4"96 />97 </label>98 </div>99100 {/* Select Dropdown */}101 <div>102 <label>103 Country:104 <select105 name="country"106 value={formData.country}107 onChange={handleInputChange}108 >109 <option value="">-- Select --</option>110 <option value="us">United States</option>111 <option value="uk">United Kingdom</option>112 <option value="ca">Canada</option>113 </select>114 </label>115 </div>116117 {/* Checkbox */}118 <div>119 <label>120 <input121 type="checkbox"122 name="newsletter"123 checked={formData.newsletter}124 onChange={handleInputChange}125 />126 Subscribe to newsletter127 </label>128 </div>129130 {/* Radio Buttons */}131 <fieldset>132 <legend>Experience Level:</legend>133 {['beginner', 'intermediate', 'advanced'].map(level => (134 <label key={level}>135 <input136 type="radio"137 name="experience"138 value={level}139 checked={formData.experience === level}140 onChange={handleInputChange}141 />142 {level.charAt(0).toUpperCase() + level.slice(1)}143 </label>144 ))}145 </fieldset>146147 {/* Multiple Checkboxes */}148 <fieldset>149 <legend>Interests:</legend>150 {['React', 'Vue', 'Angular'].map(tech => (151 <label key={tech}>152 <input153 type="checkbox"154 checked={formData.interests.includes(tech)}155 onChange={() => handleInterestChange(tech)}156 />157 {tech}158 </label>159 ))}160 </fieldset>161162 <button type="submit">Submit</button>163164 {/* Display current form state */}165 <div style={{ marginTop: '20px' }}>166 <h4>Form State:</h4>167 <pre>{JSON.stringify(formData, null, 2)}</pre>168 </div>169 </form>170 );171}172173// Additional examples showing input formatting174function FormattedInputExample() {175 const [phone, setPhone] = useState('');176177 // Format phone number as (123) 456-7890178 const formatPhoneNumber = (value) => {179 const phoneNumber = value.replace(/D/g, '');180 if (phoneNumber.length < 4) return phoneNumber;181 if (phoneNumber.length < 7) {182 return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3)}`;183 }184 return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6, 10)}`;185 };186187 return (188 <div>189 <label>190 Phone Number:191 <input192 type="tel"193 value={phone}194 onChange={(e) => setPhone(formatPhoneNumber(e.target.value))}195 placeholder="(123) 456-7890"196 />197 </label>198 </div>199 );200}201202// Dynamic form fields example203function DynamicFormFields() {204 const [participants, setParticipants] = useState([205 { id: 1, name: '', email: '' }206 ]);207208 const addParticipant = () => {209 const newId = participants.length + 1;210 setParticipants([...participants, { id: newId, name: '', email: '' }]);211 };212213 const removeParticipant = (id) => {214 setParticipants(participants.filter(p => p.id !== id));215 };216217 const updateParticipant = (id, field, value) => {218 setParticipants(participants.map(p =>219 p.id === id ? { ...p, [field]: value } : p220 ));221 };222223 return (224 <div>225 {participants.map((participant, index) => (226 <div key={participant.id}>227 <h4>Participant {index + 1}</h4>228 <input229 type="text"230 placeholder="Name"231 value={participant.name}232 onChange={(e) => updateParticipant(participant.id, 'name', e.target.value)}233 />234 <input235 type="email"236 placeholder="Email"237 value={participant.email}238 onChange={(e) => updateParticipant(participant.id, 'email', e.target.value)}239 />240 {participants.length > 1 && (241 <button onClick={() => removeParticipant(participant.id)}>242 Remove243 </button>244 )}245 </div>246 ))}247 <button onClick={addParticipant}>Add Participant</button>248 </div>249 );250}251252// 💡 KEY TAKEAWAYS:253// 1. Controlled components give you full control over form inputs254// 2. React state is the single source of truth255// 3. You can format, validate, and transform input in real-time256// 4. Dynamic and conditional forms are easy to implement
Form Validation: Ensuring Data Quality
Form validation is crucial for ensuring users provide correct and complete information. React gives us the flexibility to implement validation in multiple ways, from simple required fields to complex business rules. Let's explore how to build robust form validation that provides great user experience.
Why Validation Matters Good validation:
- Prevents errors: Catches mistakes before they reach your server
- Saves time: Users fix issues immediately instead of after submission
- Improves UX: Clear feedback helps users succeed
- Reduces server load: Invalid data never gets sent
- Ensures data quality: Your database stays clean and consistent
Types of Validation
- Field-level validation: Check individual fields as users type
- Form-level validation: Check the entire form before submission
- Async validation: Check with server (username availability, etc.)
- Cross-field validation: Validate fields that depend on each other
When to Validate
- On change: Immediate feedback as users type
- On blur: When users leave a field
- On submit: Final check before sending data
- Debounced: After user stops typing for a moment
Validation Best Practices
- Show errors clearly but not aggressively
- Provide helpful error messages
- Indicate required fields
- Show success states too
- Don't validate empty optional fields
- Consider accessibility (screen readers)
1// 🎯 FORM VALIDATION EXAMPLE23import React, { useState, useEffect } from 'react';45function ValidationExample() {6 const [formData, setFormData] = useState({7 email: '',8 password: '',9 confirmPassword: ''10 });1112 const [errors, setErrors] = useState({});13 const [touched, setTouched] = useState({});1415 // Validation logic16 const validateField = (name, value) => {17 switch (name) {18 case 'email':19 if (!value) return 'Email is required';20 if (!/S+@S+.S+/.test(value)) return 'Email is invalid';21 return '';2223 case 'password':24 if (!value) return 'Password is required';25 if (value.length < 8) return 'Password must be at least 8 characters';26 if (!/[A-Z]/.test(value)) return 'Password must contain uppercase';27 if (!/[a-z]/.test(value)) return 'Password must contain lowercase';28 if (!/[0-9]/.test(value)) return 'Password must contain a number';29 return '';3031 case 'confirmPassword':32 if (!value) return 'Please confirm your password';33 if (value !== formData.password) return 'Passwords do not match';34 return '';3536 default:37 return '';38 }39 };4041 const handleChange = (e) => {42 const { name, value } = e.target;43 setFormData(prev => ({ ...prev, [name]: value }));4445 // Validate if field has been touched46 if (touched[name]) {47 setErrors(prev => ({ ...prev, [name]: validateField(name, value) }));48 }49 };5051 const handleBlur = (e) => {52 const { name, value } = e.target;53 setTouched(prev => ({ ...prev, [name]: true }));54 setErrors(prev => ({ ...prev, [name]: validateField(name, value) }));55 };5657 const handleSubmit = (e) => {58 e.preventDefault();5960 // Validate all fields61 const newErrors = {};62 Object.keys(formData).forEach(key => {63 const error = validateField(key, formData[key]);64 if (error) newErrors[key] = error;65 });6667 setErrors(newErrors);68 setTouched({ email: true, password: true, confirmPassword: true });6970 if (Object.keys(newErrors).length === 0) {71 console.log('Form is valid! Submitting...', formData);72 }73 };7475 return (76 <form onSubmit={handleSubmit}>77 <div>78 <label>79 Email:80 <input81 type="email"82 name="email"83 value={formData.email}84 onChange={handleChange}85 onBlur={handleBlur}86 style={{ borderColor: errors.email && touched.email ? 'red' : '' }}87 />88 </label>89 {errors.email && touched.email && (90 <span style={{ color: 'red', fontSize: '14px' }}>{errors.email}</span>91 )}92 </div>9394 <div>95 <label>96 Password:97 <input98 type="password"99 name="password"100 value={formData.password}101 onChange={handleChange}102 onBlur={handleBlur}103 style={{ borderColor: errors.password && touched.password ? 'red' : '' }}104 />105 </label>106 {errors.password && touched.password && (107 <span style={{ color: 'red', fontSize: '14px' }}>{errors.password}</span>108 )}109 </div>110111 <div>112 <label>113 Confirm Password:114 <input115 type="password"116 name="confirmPassword"117 value={formData.confirmPassword}118 onChange={handleChange}119 onBlur={handleBlur}120 style={{ borderColor: errors.confirmPassword && touched.confirmPassword ? 'red' : '' }}121 />122 </label>123 {errors.confirmPassword && touched.confirmPassword && (124 <span style={{ color: 'red', fontSize: '14px' }}>{errors.confirmPassword}</span>125 )}126 </div>127128 <button type="submit">Submit</button>129 </form>130 );131}132133// Async validation example134function AsyncValidation() {135 const [username, setUsername] = useState('');136 const [isChecking, setIsChecking] = useState(false);137 const [isAvailable, setIsAvailable] = useState(null);138139 // Debounced username check140 useEffect(() => {141 if (!username || username.length < 3) {142 setIsAvailable(null);143 return;144 }145146 const timeoutId = setTimeout(async () => {147 setIsChecking(true);148149 // Simulate API call150 await new Promise(resolve => setTimeout(resolve, 1000));151152 // Check if username is taken153 const taken = ['admin', 'user', 'test'].includes(username.toLowerCase());154 setIsAvailable(!taken);155 setIsChecking(false);156 }, 500);157158 return () => clearTimeout(timeoutId);159 }, [username]);160161 return (162 <div>163 <label>164 Username:165 <input166 type="text"167 value={username}168 onChange={(e) => setUsername(e.target.value)}169 placeholder="Check availability"170 />171 {isChecking && <span> Checking...</span>}172 {!isChecking && isAvailable === true && <span style={{ color: 'green' }}> ✓ Available</span>}173 {!isChecking && isAvailable === false && <span style={{ color: 'red' }}> ✗ Taken</span>}174 </label>175 </div>176 );177}178179// 💡 KEY VALIDATION TAKEAWAYS:180// 1. Validate on blur for better UX181// 2. Show errors only after user interaction (touched)182// 3. Validate all fields on submit183// 4. Use clear, helpful error messages184// 5. Consider async validation for unique fields
Custom Form Hooks: Reusable Form Logic
As you build more forms, you'll notice patterns emerging. Custom hooks allow you to extract and reuse form logic across different components. This makes your code more maintainable and your forms more consistent.
What Are Custom Hooks? Custom hooks are JavaScript functions that:
- Start with "use" (like useForm, useValidation)
- Can call other hooks
- Return values and functions for your components to use
- Encapsulate complex logic in a reusable way
Benefits of Custom Form Hooks
- Code Reusability: Write validation logic once, use it everywhere
- Consistency: All forms behave the same way
- Separation of Concerns: UI components stay focused on rendering
- Easier Testing: Test form logic independently
- Better Organization: Keep form logic in one place
Common Custom Hook Patterns
- useForm: Manages form state and validation
- useField: Manages individual field state
- useValidation: Handles validation rules
- useFormSubmit: Manages submission state and API calls
- useDebounce: Delays validation or API calls
When to Create Custom Hooks Create a custom hook when you:
- Repeat the same logic in multiple components
- Have complex state management logic
- Want to share stateful logic between components
- Need to organize complex component logic
1// 🎯 CUSTOM FORM HOOKS EXAMPLE23import React, { useState, useEffect, useCallback } from 'react';45// Custom hook for form management6function useForm(initialValues, validate) {7 const [values, setValues] = useState(initialValues);8 const [errors, setErrors] = useState({});9 const [touched, setTouched] = useState({});10 const [isSubmitting, setIsSubmitting] = useState(false);1112 // Handle input changes13 const handleChange = useCallback((e) => {14 const { name, value, type, checked } = e.target;15 const fieldValue = type === 'checkbox' ? checked : value;1617 setValues(prev => ({18 ...prev,19 [name]: fieldValue20 }));2122 // Validate on change if field was touched23 if (touched[name] && validate) {24 const validationErrors = validate({ ...values, [name]: fieldValue });25 setErrors(prev => ({26 ...prev,27 [name]: validationErrors[name]28 }));29 }30 }, [values, touched, validate]);3132 // Handle blur events33 const handleBlur = useCallback((e) => {34 const { name } = e.target;35 setTouched(prev => ({ ...prev, [name]: true }));3637 if (validate) {38 const validationErrors = validate(values);39 setErrors(prev => ({40 ...prev,41 [name]: validationErrors[name]42 }));43 }44 }, [values, validate]);4546 // Handle form submission47 const handleSubmit = useCallback((onSubmit) => async (e) => {48 e.preventDefault();49 setIsSubmitting(true);5051 // Touch all fields52 const touchedAll = {};53 Object.keys(values).forEach(key => {54 touchedAll[key] = true;55 });56 setTouched(touchedAll);5758 // Validate all fields59 const validationErrors = validate ? validate(values) : {};60 setErrors(validationErrors);6162 // Submit if no errors63 if (Object.keys(validationErrors).length === 0) {64 await onSubmit(values);65 }6667 setIsSubmitting(false);68 }, [values, validate]);6970 // Reset form71 const resetForm = useCallback(() => {72 setValues(initialValues);73 setErrors({});74 setTouched({});75 setIsSubmitting(false);76 }, [initialValues]);7778 return {79 values,80 errors,81 touched,82 isSubmitting,83 handleChange,84 handleBlur,85 handleSubmit,86 resetForm87 };88}8990// Example: Contact form using custom hook91function ContactForm() {92 // Validation function93 const validate = (values) => {94 const errors = {};9596 if (!values.name) {97 errors.name = 'Name is required';98 } else if (values.name.length < 2) {99 errors.name = 'Name must be at least 2 characters';100 }101102 if (!values.email) {103 errors.email = 'Email is required';104 } else if (!/S+@S+.S+/.test(values.email)) {105 errors.email = 'Email is invalid';106 }107108 if (!values.message) {109 errors.message = 'Message is required';110 } else if (values.message.length < 10) {111 errors.message = 'Message must be at least 10 characters';112 }113114 return errors;115 };116117 // Use the custom hook118 const {119 values,120 errors,121 touched,122 isSubmitting,123 handleChange,124 handleBlur,125 handleSubmit,126 resetForm127 } = useForm(128 { name: '', email: '', message: '' },129 validate130 );131132 // Submit handler133 const onSubmit = async (formValues) => {134 console.log('Submitting:', formValues);135 // Simulate API call136 await new Promise(resolve => setTimeout(resolve, 1000));137 alert('Form submitted successfully!');138 resetForm();139 };140141 return (142 <form onSubmit={handleSubmit(onSubmit)}>143 <div>144 <input145 type="text"146 name="name"147 placeholder="Your Name"148 value={values.name}149 onChange={handleChange}150 onBlur={handleBlur}151 />152 {errors.name && touched.name && (153 <span style={{ color: 'red' }}>{errors.name}</span>154 )}155 </div>156157 <div>158 <input159 type="email"160 name="email"161 placeholder="Your Email"162 value={values.email}163 onChange={handleChange}164 onBlur={handleBlur}165 />166 {errors.email && touched.email && (167 <span style={{ color: 'red' }}>{errors.email}</span>168 )}169 </div>170171 <div>172 <textarea173 name="message"174 placeholder="Your Message"175 value={values.message}176 onChange={handleChange}177 onBlur={handleBlur}178 rows="4"179 />180 {errors.message && touched.message && (181 <span style={{ color: 'red' }}>{errors.message}</span>182 )}183 </div>184185 <button type="submit" disabled={isSubmitting}>186 {isSubmitting ? 'Sending...' : 'Send Message'}187 </button>188 <button type="button" onClick={resetForm}>189 Reset190 </button>191 </form>192 );193}194195// Custom hook for debouncing196function useDebounce(value, delay) {197 const [debouncedValue, setDebouncedValue] = useState(value);198199 useEffect(() => {200 const handler = setTimeout(() => {201 setDebouncedValue(value);202 }, delay);203204 return () => clearTimeout(handler);205 }, [value, delay]);206207 return debouncedValue;208}209210// 💡 CUSTOM HOOK BEST PRACTICES:211// 1. Always prefix custom hooks with "use"212// 2. Keep hooks focused on a single responsibility213// 3. Return an object for easier destructuring214// 4. Make hooks reusable across components215// 5. Handle cleanup in useEffect hooks