Master form handling in React with controlled components, validation patterns, and advanced input management. Build professional forms with great user experiences.
Forms are the primary way users interact with web applications - from simple contact forms to complex multi-step wizards. In React, we use controlled components to manage form state, giving us complete control over user input, validation, and submission.
Think of controlled components as having React be the "single source of truth" for form data. Instead of letting the DOM manage input values, React state controls everything. This approach enables powerful features like real-time validation, conditional fields, dynamic forms, and seamless data flow.
In this comprehensive lesson, you'll master React form patterns used in professional applications - from basic controlled inputs to complex form state management, validation strategies, and accessibility best practices.
React offers two approaches to handling form inputs: controlled and uncontrolled components. Understanding the difference is crucial for effective form management.
Controlled Components (Recommended):
In controlled components, React state controls the input value. Every keystroke triggers a state update, and the input displays the current state value.
1function ControlledInput() {
2 const [value, setValue] = useState('');
3
4 const handleChange = (event) => {
5 setValue(event.target.value);
6 };
7
8 return (
9 <div>
10 <input
11 type="text"
12 value={value} // State controls the value
13 onChange={handleChange} // State updates on change
14 placeholder="Enter your name"
15 />
16 <p>Current value: {value}</p>
17 </div>
18 );
19}Uncontrolled Components:
Uncontrolled components let the DOM handle the input value. You use refs to access the current value when needed.
1function UncontrolledInput() {
2 const inputRef = useRef(null);
3
4 const handleSubmit = (event) => {
5 event.preventDefault();
6 console.log('Input value:', inputRef.current.value);
7 };
8
9 return (
10 <form onSubmit={handleSubmit}>
11 <input
12 ref={inputRef}
13 type="text"
14 defaultValue="Initial value"
15 placeholder="Enter your name"
16 />
17 <button type="submit">Submit</button>
18 </form>
19 );
20}When to Use Each:
Use Controlled Components When:
Use Uncontrolled Components When:
Best Practice: Default to controlled components for most use cases. They provide better user experience and are easier to test and debug.
React can handle all HTML input types with controlled components. Each input type has specific patterns for optimal user experience.
Text Inputs (text, email, password, textarea):
1function TextInputs() {
2 const [formData, setFormData] = useState({
3 name: '',
4 email: '',
5 password: '',
6 bio: ''
7 });
8
9 const handleChange = (event) => {
10 const { name, value } = event.target;
11 setFormData(prev => ({
12 ...prev,
13 [name]: value
14 }));
15 };
16
17 return (
18 <form>
19 <input
20 type="text"
21 name="name"
22 value={formData.name}
23 onChange={handleChange}
24 placeholder="Full Name"
25 />
26
27 <input
28 type="email"
29 name="email"
30 value={formData.email}
31 onChange={handleChange}
32 placeholder="Email Address"
33 />
34
35 <input
36 type="password"
37 name="password"
38 value={formData.password}
39 onChange={handleChange}
40 placeholder="Password"
41 />
42
43 <textarea
44 name="bio"
45 value={formData.bio}
46 onChange={handleChange}
47 placeholder="Tell us about yourself"
48 rows={4}
49 />
50 </form>
51 );
52}Checkboxes and Radio Buttons:
1function SelectionInputs() {
2 const [preferences, setPreferences] = useState({
3 newsletter: false,
4 notifications: true,
5 theme: 'light',
6 languages: []
7 });
8
9 const handleCheckboxChange = (event) => {
10 const { name, checked } = event.target;
11 setPreferences(prev => ({
12 ...prev,
13 [name]: checked
14 }));
15 };
16
17 const handleRadioChange = (event) => {
18 setPreferences(prev => ({
19 ...prev,
20 theme: event.target.value
21 }));
22 };
23
24 const handleLanguageChange = (language) => {
25 setPreferences(prev => ({
26 ...prev,
27 languages: prev.languages.includes(language)
28 ? prev.languages.filter(lang => lang !== language)
29 : [...prev.languages, language]
30 }));
31 };
32
33 return (
34 <form>
35 {/* Checkboxes */}
36 <div>
37 <h3>Email Preferences</h3>
38 <label>
39 <input
40 type="checkbox"
41 name="newsletter"
42 checked={preferences.newsletter}
43 onChange={handleCheckboxChange}
44 />
45 Subscribe to newsletter
46 </label>
47 </div>
48
49 {/* Radio Buttons */}
50 <div>
51 <h3>Theme Preference</h3>
52 {['light', 'dark', 'auto'].map(theme => (
53 <label key={theme}>
54 <input
55 type="radio"
56 name="theme"
57 value={theme}
58 checked={preferences.theme === theme}
59 onChange={handleRadioChange}
60 />
61 {theme.charAt(0).toUpperCase() + theme.slice(1)}
62 </label>
63 ))}
64 </div>
65
66 {/* Multiple Checkboxes (Array) */}
67 <div>
68 <h3>Programming Languages</h3>
69 {['JavaScript', 'Python', 'Java', 'C++'].map(language => (
70 <label key={language}>
71 <input
72 type="checkbox"
73 checked={preferences.languages.includes(language)}
74 onChange={() => handleLanguageChange(language)}
75 />
76 {language}
77 </label>
78 ))}
79 </div>
80 </form>
81 );
82}Select Dropdowns:
1function SelectInputs() {
2 const [selections, setSelections] = useState({
3 country: '',
4 skills: []
5 });
6
7 const handleSelectChange = (event) => {
8 const { name, value } = event.target;
9 setSelections(prev => ({
10 ...prev,
11 [name]: value
12 }));
13 };
14
15 const handleMultiSelectChange = (event) => {
16 const { options } = event.target;
17 const values = Array.from(options)
18 .filter(option => option.selected)
19 .map(option => option.value);
20
21 setSelections(prev => ({
22 ...prev,
23 skills: values
24 }));
25 };
26
27 return (
28 <form>
29 {/* Single Select */}
30 <select
31 name="country"
32 value={selections.country}
33 onChange={handleSelectChange}
34 >
35 <option value="">Select a country</option>
36 <option value="us">United States</option>
37 <option value="ca">Canada</option>
38 <option value="uk">United Kingdom</option>
39 <option value="de">Germany</option>
40 </select>
41
42 {/* Multi-Select */}
43 <select
44 name="skills"
45 multiple
46 value={selections.skills}
47 onChange={handleMultiSelectChange}
48 >
49 <option value="react">React</option>
50 <option value="vue">Vue</option>
51 <option value="angular">Angular</option>
52 <option value="node">Node.js</option>
53 </select>
54 </form>
55 );
56}Effective form validation provides immediate feedback to users and prevents invalid data submission. React enables both real-time and submission validation.
Basic Validation Hook:
1function useFormValidation(initialState, validateFunction) {
2 const [values, setValues] = useState(initialState);
3 const [errors, setErrors] = useState({});
4 const [isSubmitting, setIsSubmitting] = useState(false);
5
6 const handleChange = (event) => {
7 const { name, value } = event.target;
8 setValues(prev => ({
9 ...prev,
10 [name]: value
11 }));
12
13 // Clear error when user starts typing
14 if (errors[name]) {
15 setErrors(prev => ({
16 ...prev,
17 [name]: ''
18 }));
19 }
20 };
21
22 const handleSubmit = (callback) => async (event) => {
23 event.preventDefault();
24 setIsSubmitting(true);
25
26 const validationErrors = validateFunction(values);
27 setErrors(validationErrors);
28
29 if (Object.keys(validationErrors).length === 0) {
30 await callback(values);
31 }
32
33 setIsSubmitting(false);
34 };
35
36 return {
37 values,
38 errors,
39 isSubmitting,
40 handleChange,
41 handleSubmit
42 };
43}Comprehensive Form with Validation:
1function ContactForm() {
2 const validateForm = (values) => {
3 const errors = {};
4
5 // Name validation
6 if (!values.name.trim()) {
7 errors.name = 'Name is required';
8 } else if (values.name.length < 2) {
9 errors.name = 'Name must be at least 2 characters';
10 }
11
12 // Email validation
13 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
14 if (!values.email.trim()) {
15 errors.email = 'Email is required';
16 } else if (!emailRegex.test(values.email)) {
17 errors.email = 'Please enter a valid email address';
18 }
19
20 // Phone validation
21 const phoneRegex = /^\(\d{3}\) \d{3}-\d{4}$/;
22 if (!values.phone.trim()) {
23 errors.phone = 'Phone number is required';
24 } else if (!phoneRegex.test(values.phone)) {
25 errors.phone = 'Phone must be in format (123) 456-7890';
26 }
27
28 return errors;
29 };
30
31 const {
32 values,
33 errors,
34 isSubmitting,
35 handleChange,
36 handleSubmit
37 } = useFormValidation({
38 name: '',
39 email: '',
40 phone: '',
41 message: ''
42 }, validateForm);
43
44 const submitForm = async (formData) => {
45 try {
46 const response = await fetch('/api/contact', {
47 method: 'POST',
48 headers: {
49 'Content-Type': 'application/json'
50 },
51 body: JSON.stringify(formData)
52 });
53
54 if (response.ok) {
55 alert('Message sent successfully!');
56 } else {
57 throw new Error('Failed to send message');
58 }
59 } catch (error) {
60 alert('Error sending message. Please try again.');
61 }
62 };
63
64 return (
65 <form onSubmit={handleSubmit(submitForm)}>
66 <div>
67 <input
68 type="text"
69 name="name"
70 value={values.name}
71 onChange={handleChange}
72 placeholder="Full Name"
73 className={`input ${errors.name ? 'error' : ''}`}
74 />
75 {errors.name && <p className="error-message">{errors.name}</p>}
76 </div>
77
78 <div>
79 <input
80 type="email"
81 name="email"
82 value={values.email}
83 onChange={handleChange}
84 placeholder="Email Address"
85 className={`input ${errors.email ? 'error' : ''}`}
86 />
87 {errors.email && <p className="error-message">{errors.email}</p>}
88 </div>
89
90 <button type="submit" disabled={isSubmitting}>
91 {isSubmitting ? 'Sending...' : 'Send Message'}
92 </button>
93 </form>
94 );
95}Professional forms require sophisticated patterns for complex scenarios like dynamic fields, multi-step forms, and optimal user experience.
Dynamic Form Fields:
1function DynamicForm() {
2 const [formData, setFormData] = useState({
3 name: '',
4 email: '',
5 skills: [{ name: '', level: 'beginner' }],
6 hasExperience: false,
7 experienceYears: ''
8 });
9
10 const addSkill = () => {
11 setFormData(prev => ({
12 ...prev,
13 skills: [...prev.skills, { name: '', level: 'beginner' }]
14 }));
15 };
16
17 const removeSkill = (index) => {
18 setFormData(prev => ({
19 ...prev,
20 skills: prev.skills.filter((_, i) => i !== index)
21 }));
22 };
23
24 const updateSkill = (index, field, value) => {
25 setFormData(prev => ({
26 ...prev,
27 skills: prev.skills.map((skill, i) =>
28 i === index ? { ...skill, [field]: value } : skill
29 )
30 }));
31 };
32
33 return (
34 <form>
35 {/* Dynamic Skills Section */}
36 <div>
37 <h3>Skills</h3>
38 {formData.skills.map((skill, index) => (
39 <div key={index} className="skill-row">
40 <input
41 type="text"
42 value={skill.name}
43 onChange={(e) => updateSkill(index, 'name', e.target.value)}
44 placeholder="Skill name"
45 />
46 <select
47 value={skill.level}
48 onChange={(e) => updateSkill(index, 'level', e.target.value)}
49 >
50 <option value="beginner">Beginner</option>
51 <option value="intermediate">Intermediate</option>
52 <option value="advanced">Advanced</option>
53 <option value="expert">Expert</option>
54 </select>
55 {formData.skills.length > 1 && (
56 <button
57 type="button"
58 onClick={() => removeSkill(index)}
59 >
60 Remove
61 </button>
62 )}
63 </div>
64 ))}
65 <button type="button" onClick={addSkill}>
66 + Add Skill
67 </button>
68 </div>
69
70 {/* Conditional Fields */}
71 <div>
72 <label>
73 <input
74 type="checkbox"
75 checked={formData.hasExperience}
76 onChange={(e) => setFormData(prev => ({
77 ...prev,
78 hasExperience: e.target.checked,
79 experienceYears: e.target.checked ? prev.experienceYears : ''
80 }))}
81 />
82 I have professional experience
83 </label>
84
85 {formData.hasExperience && (
86 <input
87 type="number"
88 value={formData.experienceYears}
89 onChange={(e) => setFormData(prev => ({
90 ...prev,
91 experienceYears: e.target.value
92 }))}
93 placeholder="Years of experience"
94 min="0"
95 max="50"
96 />
97 )}
98 </div>
99 </form>
100 );
101}File Upload Pattern:
1function FileUploadForm() {
2 const [files, setFiles] = useState([]);
3 const [uploading, setUploading] = useState(false);
4
5 const handleFileSelect = (event) => {
6 const selectedFiles = Array.from(event.target.files);
7
8 // Validate files
9 const validFiles = selectedFiles.filter(file => {
10 const isValidType = file.type.startsWith('image/');
11 const isValidSize = file.size <= 5 * 1024 * 1024; // 5MB limit
12 return isValidType && isValidSize;
13 });
14
15 setFiles(prev => [...prev, ...validFiles]);
16 };
17
18 const handleSubmit = async (event) => {
19 event.preventDefault();
20 setUploading(true);
21
22 const formData = new FormData();
23 files.forEach(file => {
24 formData.append('files', file);
25 });
26
27 try {
28 const response = await fetch('/api/upload', {
29 method: 'POST',
30 body: formData
31 });
32
33 if (response.ok) {
34 alert('Files uploaded successfully!');
35 setFiles([]);
36 }
37 } catch (error) {
38 alert('Upload failed');
39 } finally {
40 setUploading(false);
41 }
42 };
43
44 return (
45 <form onSubmit={handleSubmit}>
46 <input
47 type="file"
48 multiple
49 accept="image/*"
50 onChange={handleFileSelect}
51 />
52
53 <button
54 type="submit"
55 disabled={files.length === 0 || uploading}
56 >
57 {uploading ? 'Uploading...' : 'Upload Files'}
58 </button>
59 </form>
60 );
61}Form Best Practices:
Build a comprehensive registration form demonstrating all form patterns: controlled inputs, validation, dynamic fields, and great user experience.
1// Complete User Registration Form
2function UserRegistrationForm() {
3 const [formData, setFormData] = useState({
4 // Personal Info
5 firstName: '',
6 lastName: '',
7 email: '',
8 password: '',
9 confirmPassword: '',
10
11 // Contact Info
12 phone: '',
13 country: '',
14
15 // Preferences
16 interests: [],
17 newsletter: false,
18
19 // Skills
20 skills: [{ name: '', level: 'beginner' }],
21
22 // Profile
23 bio: ''
24 });
25
26 const [errors, setErrors] = useState({});
27 const [isSubmitting, setIsSubmitting] = useState(false);
28 const [step, setStep] = useState(1);
29 const totalSteps = 3;
30
31 // Validation function
32 const validateStep = (stepNumber) => {
33 const newErrors = {};
34
35 switch (stepNumber) {
36 case 1: // Personal Info
37 if (!formData.firstName.trim()) {
38 newErrors.firstName = 'First name is required';
39 }
40 if (!formData.email.trim()) {
41 newErrors.email = 'Email is required';
42 } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
43 newErrors.email = 'Email is invalid';
44 }
45 if (!formData.password || formData.password.length < 8) {
46 newErrors.password = 'Password must be at least 8 characters';
47 }
48 if (formData.password !== formData.confirmPassword) {
49 newErrors.confirmPassword = 'Passwords do not match';
50 }
51 break;
52
53 case 2: // Contact & Preferences
54 if (!formData.phone.trim()) {
55 newErrors.phone = 'Phone number is required';
56 }
57 if (formData.interests.length === 0) {
58 newErrors.interests = 'Select at least one interest';
59 }
60 break;
61
62 case 3: // Skills & Profile
63 if (formData.skills.some(skill => !skill.name.trim())) {
64 newErrors.skills = 'All skills must have a name';
65 }
66 if (!formData.bio.trim() || formData.bio.length < 50) {
67 newErrors.bio = 'Bio must be at least 50 characters';
68 }
69 break;
70 }
71
72 setErrors(newErrors);
73 return Object.keys(newErrors).length === 0;
74 };
75
76 // Handle input changes
77 const handleChange = (event) => {
78 const { name, value, type, checked } = event.target;
79 setFormData(prev => ({
80 ...prev,
81 [name]: type === 'checkbox' ? checked : value
82 }));
83
84 // Clear error when user starts typing
85 if (errors[name]) {
86 setErrors(prev => ({ ...prev, [name]: '' }));
87 }
88 };
89
90 // Handle array changes (interests)
91 const handleInterestChange = (interest) => {
92 setFormData(prev => ({
93 ...prev,
94 interests: prev.interests.includes(interest)
95 ? prev.interests.filter(i => i !== interest)
96 : [...prev.interests, interest]
97 }));
98 };
99
100 // Handle skills array
101 const addSkill = () => {
102 setFormData(prev => ({
103 ...prev,
104 skills: [...prev.skills, { name: '', level: 'beginner' }]
105 }));
106 };
107
108 const removeSkill = (index) => {
109 setFormData(prev => ({
110 ...prev,
111 skills: prev.skills.filter((_, i) => i !== index)
112 }));
113 };
114
115 const updateSkill = (index, field, value) => {
116 setFormData(prev => ({
117 ...prev,
118 skills: prev.skills.map((skill, i) =>
119 i === index ? { ...skill, [field]: value } : skill
120 )
121 }));
122 };
123
124 // Navigation
125 const nextStep = () => {
126 if (validateStep(step)) {
127 setStep(prev => Math.min(prev + 1, totalSteps));
128 }
129 };
130
131 const prevStep = () => {
132 setStep(prev => Math.max(prev - 1, 1));
133 };
134
135 // Submit form
136 const handleSubmit = async (event) => {
137 event.preventDefault();
138 if (!validateStep(step)) return;
139
140 setIsSubmitting(true);
141 try {
142 // Simulate API call
143 await new Promise(resolve => setTimeout(resolve, 2000));
144 alert('Registration successful! 🎉');
145
146 // Reset form
147 setFormData({
148 firstName: '', lastName: '', email: '', password: '', confirmPassword: '',
149 phone: '', country: '', interests: [], newsletter: false,
150 skills: [{ name: '', level: 'beginner' }], bio: ''
151 });
152 setStep(1);
153 } catch (error) {
154 alert('Registration failed. Please try again.');
155 } finally {
156 setIsSubmitting(false);
157 }
158 };
159
160 return (
161 <div className="max-w-2xl mx-auto p-6">
162 {/* Progress Bar */}
163 <div className="mb-8">
164 <div className="flex justify-between text-sm mb-2">
165 <span>Step {step} of {totalSteps}</span>
166 <span>{Math.round((step / totalSteps) * 100)}% Complete</span>
167 </div>
168 <div className="w-full bg-gray-200 rounded-full h-2">
169 <div
170 className="bg-blue-500 h-2 rounded-full transition-all duration-300"
171 style={{ width: `${(step / totalSteps) * 100}%` }}
172 ></div>
173 </div>
174 </div>
175
176 <form onSubmit={handleSubmit}>
177 {/* Step 1: Personal Information */}
178 {step === 1 && (
179 <div className="space-y-6">
180 <h2 className="text-2xl font-bold">Personal Information</h2>
181
182 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
183 <div>
184 <label className="block text-sm font-medium mb-2">First Name *</label>
185 <input
186 type="text"
187 name="firstName"
188 value={formData.firstName}
189 onChange={handleChange}
190 className={`w-full px-3 py-2 border rounded-md ${
191 errors.firstName ? 'border-red-500' : 'border-gray-300'
192 }`}
193 />
194 {errors.firstName && <p className="mt-1 text-sm text-red-500">{errors.firstName}</p>}
195 </div>
196
197 <div>
198 <label className="block text-sm font-medium mb-2">Last Name</label>
199 <input
200 type="text"
201 name="lastName"
202 value={formData.lastName}
203 onChange={handleChange}
204 className="w-full px-3 py-2 border border-gray-300 rounded-md"
205 />
206 </div>
207 </div>
208
209 <div>
210 <label className="block text-sm font-medium mb-2">Email Address *</label>
211 <input
212 type="email"
213 name="email"
214 value={formData.email}
215 onChange={handleChange}
216 className={`w-full px-3 py-2 border rounded-md ${
217 errors.email ? 'border-red-500' : 'border-gray-300'
218 }`}
219 />
220 {errors.email && <p className="mt-1 text-sm text-red-500">{errors.email}</p>}
221 </div>
222
223 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
224 <div>
225 <label className="block text-sm font-medium mb-2">Password *</label>
226 <input
227 type="password"
228 name="password"
229 value={formData.password}
230 onChange={handleChange}
231 className={`w-full px-3 py-2 border rounded-md ${
232 errors.password ? 'border-red-500' : 'border-gray-300'
233 }`}
234 />
235 {errors.password && <p className="mt-1 text-sm text-red-500">{errors.password}</p>}
236 </div>
237
238 <div>
239 <label className="block text-sm font-medium mb-2">Confirm Password *</label>
240 <input
241 type="password"
242 name="confirmPassword"
243 value={formData.confirmPassword}
244 onChange={handleChange}
245 className={`w-full px-3 py-2 border rounded-md ${
246 errors.confirmPassword ? 'border-red-500' : 'border-gray-300'
247 }`}
248 />
249 {errors.confirmPassword && <p className="mt-1 text-sm text-red-500">{errors.confirmPassword}</p>}
250 </div>
251 </div>
252 </div>
253 )}
254
255 {/* Step 2: Contact & Preferences */}
256 {step === 2 && (
257 <div className="space-y-6">
258 <h2 className="text-2xl font-bold">Contact & Preferences</h2>
259
260 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
261 <div>
262 <label className="block text-sm font-medium mb-2">Phone Number *</label>
263 <input
264 type="tel"
265 name="phone"
266 value={formData.phone}
267 onChange={handleChange}
268 placeholder="(123) 456-7890"
269 className={`w-full px-3 py-2 border rounded-md ${
270 errors.phone ? 'border-red-500' : 'border-gray-300'
271 }`}
272 />
273 {errors.phone && <p className="mt-1 text-sm text-red-500">{errors.phone}</p>}
274 </div>
275
276 <div>
277 <label className="block text-sm font-medium mb-2">Country</label>
278 <select
279 name="country"
280 value={formData.country}
281 onChange={handleChange}
282 className="w-full px-3 py-2 border border-gray-300 rounded-md"
283 >
284 <option value="">Select a country</option>
285 <option value="us">United States</option>
286 <option value="ca">Canada</option>
287 <option value="uk">United Kingdom</option>
288 <option value="de">Germany</option>
289 </select>
290 </div>
291 </div>
292
293 <div>
294 <label className="block text-sm font-medium mb-3">Interests *</label>
295 <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
296 {['Technology', 'Design', 'Business', 'Marketing', 'Science', 'Arts'].map(interest => (
297 <label key={interest} className="flex items-center space-x-2">
298 <input
299 type="checkbox"
300 checked={formData.interests.includes(interest)}
301 onChange={() => handleInterestChange(interest)}
302 className="rounded"
303 />
304 <span className="text-sm">{interest}</span>
305 </label>
306 ))}
307 </div>
308 {errors.interests && <p className="mt-1 text-sm text-red-500">{errors.interests}</p>}
309 </div>
310
311 <div>
312 <label className="flex items-center space-x-2">
313 <input
314 type="checkbox"
315 name="newsletter"
316 checked={formData.newsletter}
317 onChange={handleChange}
318 className="rounded"
319 />
320 <span className="text-sm">Subscribe to our newsletter</span>
321 </label>
322 </div>
323 </div>
324 )}
325
326 {/* Step 3: Skills & Profile */}
327 {step === 3 && (
328 <div className="space-y-6">
329 <h2 className="text-2xl font-bold">Skills & Profile</h2>
330
331 <div>
332 <h3 className="text-lg font-medium mb-3">Skills</h3>
333 {formData.skills.map((skill, index) => (
334 <div key={index} className="flex gap-2 mb-2">
335 <input
336 type="text"
337 value={skill.name}
338 onChange={(e) => updateSkill(index, 'name', e.target.value)}
339 placeholder="Skill name (e.g., JavaScript)"
340 className="flex-1 px-3 py-2 border border-gray-300 rounded-md"
341 />
342 <select
343 value={skill.level}
344 onChange={(e) => updateSkill(index, 'level', e.target.value)}
345 className="px-3 py-2 border border-gray-300 rounded-md"
346 >
347 <option value="beginner">Beginner</option>
348 <option value="intermediate">Intermediate</option>
349 <option value="advanced">Advanced</option>
350 <option value="expert">Expert</option>
351 </select>
352 {formData.skills.length > 1 && (
353 <button
354 type="button"
355 onClick={() => removeSkill(index)}
356 className="px-3 py-2 text-red-500 border border-red-300 rounded-md hover:bg-red-50"
357 >
358 Remove
359 </button>
360 )}
361 </div>
362 ))}
363 <button
364 type="button"
365 onClick={addSkill}
366 className="text-blue-500 text-sm hover:underline"
367 >
368 + Add Another Skill
369 </button>
370 {errors.skills && <p className="mt-1 text-sm text-red-500">{errors.skills}</p>}
371 </div>
372
373 <div>
374 <label className="block text-sm font-medium mb-2">Bio *</label>
375 <textarea
376 name="bio"
377 value={formData.bio}
378 onChange={handleChange}
379 placeholder="Tell us about yourself..."
380 rows={4}
381 className={`w-full px-3 py-2 border rounded-md ${
382 errors.bio ? 'border-red-500' : 'border-gray-300'
383 }`}
384 />
385 <div className="flex justify-between items-center mt-1">
386 {errors.bio && <p className="text-sm text-red-500">{errors.bio}</p>}
387 <p className="text-sm text-gray-500 ml-auto">
388 {formData.bio.length}/500 characters
389 </p>
390 </div>
391 </div>
392 </div>
393 )}
394
395 {/* Navigation Buttons */}
396 <div className="flex justify-between mt-8 pt-6 border-t">
397 <button
398 type="button"
399 onClick={prevStep}
400 disabled={step === 1}
401 className="px-4 py-2 text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50"
402 >
403 Previous
404 </button>
405
406 {step < totalSteps ? (
407 <button
408 type="button"
409 onClick={nextStep}
410 className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
411 >
412 Next Step
413 </button>
414 ) : (
415 <button
416 type="submit"
417 disabled={isSubmitting}
418 className={`px-6 py-2 text-white rounded-md ${
419 isSubmitting ? 'bg-gray-400' : 'bg-green-500 hover:bg-green-600'
420 }`}
421 >
422 {isSubmitting ? 'Creating Account...' : 'Create Account'}
423 </button>
424 )}
425 </div>
426 </form>
427
428 {/* Features Summary */}
429 <div className="mt-8 p-4 bg-gray-50 rounded-lg">
430 <h3 className="font-medium mb-2">Form Features Demonstrated:</h3>
431 <ul className="text-sm text-gray-600 space-y-1">
432 <li>✅ Multi-step form with progress tracking</li>
433 <li>✅ Controlled components for all input types</li>
434 <li>✅ Real-time validation with error messages</li>
435 <li>✅ Dynamic fields (skills array)</li>
436 <li>✅ Conditional fields and complex state</li>
437 <li>✅ Loading states and user feedback</li>
438 <li>✅ Responsive design for mobile devices</li>
439 <li>✅ Accessibility features and best practices</li>
440 </ul>
441 </div>
442 </div>
443 );
444}
445
446// Main App Component
447function App() {
448 return (
449 <div className="min-h-screen bg-gray-100 py-8">
450 <UserRegistrationForm />
451 </div>
452 );
453}This comprehensive registration form demonstrates advanced React form patterns:
Multi-Step Form Architecture:
Controlled Components Mastery:
Advanced Input Types:
Validation Patterns:
User Experience Features:
State Management Techniques:
You've mastered React form handling! Next, you'll learn advanced component patterns:
You'll discover how to build component libraries and design systems that scale across large applications.
Your progress is saved in this browser, on this device only.