Introduction to Forms in React
Forms are the primary way users interact with web applications. From simple contact forms to complex multi-step workflows, mastering form handling is essential for creating engaging user experiences.
In traditional HTML, forms manage their own state and submit data directly to a server. React, with its declarative and component-based approach, gives us more control over form behavior, validation, and submission.
Controlled vs. Uncontrolled Components
There are two fundamental approaches to handling form elements in React: controlled and uncontrolled components. This distinction is central to understanding React form development.
Controlled Components
In a controlled component, React state is the "single source of truth." Form elements like inputs, selects, and textareas are controlled by React through state and event handlers.
import React, { useState } from 'react';
function ControlledForm() {
const [name, setName] = useState('');
const handleChange = (event) => {
setName(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
alert(`Hello, ${name}!`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
value={name}
onChange={handleChange}
/>
</label>
<button type="submit">Submit</button>
</form>
);
}
Analogy: A controlled component is like a puppet master controlling every movement of the puppet (form element). Nothing happens without the master's command, giving complete control but requiring more direct management.
Uncontrolled Components
In an uncontrolled component, the DOM itself maintains the form state. We access the values using refs when needed, typically during form submission.
import React, { useRef } from 'react';
function UncontrolledForm() {
const nameInputRef = useRef();
const handleSubmit = (event) => {
event.preventDefault();
alert(`Hello, ${nameInputRef.current.value}!`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
defaultValue=""
ref={nameInputRef}
/>
</label>
<button type="submit">Submit</button>
</form>
);
}
Analogy: An uncontrolled component is like giving a car to someone and only asking where they went after the trip is complete. You're not managing the journey, only collecting the final result.
Comparison Table
| Aspect | Controlled Components | Uncontrolled Components |
|---|---|---|
| Source of Truth | React state | DOM |
| Access to Current Values | Immediate (in state) | On demand (via ref) |
| Input Validation | Easy, immediate | More complex, usually at submission |
| Conditional Rendering | Simple (based on state) | More complex |
| Code Complexity | More boilerplate | Less code |
| Ideal For | Dynamic forms, real-time validation | Simple forms, file inputs |
Building a Complete Controlled Form
Let's build a more complete form that demonstrates handling multiple input types, validation, and submission.
import React, { useState } from 'react';
function RegistrationForm() {
// Form state
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: '',
role: 'user',
interests: [],
agreeToTerms: false
});
// Validation state
const [errors, setErrors] = useState({});
// Handle input changes
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
if (type === 'checkbox') {
if (name === 'agreeToTerms') {
setFormData({
...formData,
[name]: checked
});
} else {
// For checkbox groups (like interests)
const updatedInterests = [...formData.interests];
if (checked) {
updatedInterests.push(value);
} else {
const index = updatedInterests.indexOf(value);
if (index > -1) {
updatedInterests.splice(index, 1);
}
}
setFormData({
...formData,
interests: updatedInterests
});
}
} else {
setFormData({
...formData,
[name]: value
});
}
// Clear error when field is edited
if (errors[name]) {
setErrors({
...errors,
[name]: null
});
}
};
// Validate form
const validateForm = () => {
const newErrors = {};
// Username validation
if (!formData.username.trim()) {
newErrors.username = 'Username is required';
} else if (formData.username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
}
// Email validation
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
// Password validation
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
// Confirm password validation
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
// Terms agreement validation
if (!formData.agreeToTerms) {
newErrors.agreeToTerms = 'You must agree to the terms';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0; // Form is valid if no errors
};
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
// Form is valid, process submission
console.log('Form submitted:', formData);
// In a real app, you would send this data to an API
alert('Registration successful!');
} else {
console.log('Form has errors');
}
};
return (
<form onSubmit={handleSubmit} className="registration-form">
<h2>Create an Account</h2>
{/* Username field */}
<div className="form-group">
<label htmlFor="username">Username</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
className={errors.username ? 'error' : ''}
/>
{errors.username && <div className="error-message">{errors.username}</div>}
</div>
{/* Email field */}
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <div className="error-message">{errors.email}</div>}
</div>
{/* Password field */}
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
className={errors.password ? 'error' : ''}
/>
{errors.password && <div className="error-message">{errors.password}</div>}
</div>
{/* Confirm Password field */}
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
className={errors.confirmPassword ? 'error' : ''}
/>
{errors.confirmPassword && <div className="error-message">{errors.confirmPassword}</div>}
</div>
{/* Role selection */}
<div className="form-group">
<label htmlFor="role">Role</label>
<select
id="role"
name="role"
value={formData.role}
onChange={handleChange}
>
<option value="user">User</option>
<option value="editor">Editor</option>
<option value="admin">Administrator</option>
</select>
</div>
{/* Interests checkboxes */}
<div className="form-group">
<label>Interests</label>
<div className="checkbox-group">
<label>
<input
type="checkbox"
name="interests"
value="technology"
checked={formData.interests.includes('technology')}
onChange={handleChange}
/>
Technology
</label>
<label>
<input
type="checkbox"
name="interests"
value="science"
checked={formData.interests.includes('science')}
onChange={handleChange}
/>
Science
</label>
<label>
<input
type="checkbox"
name="interests"
value="arts"
checked={formData.interests.includes('arts')}
onChange={handleChange}
/>
Arts
</label>
</div>
</div>
{/* Terms agreement */}
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
name="agreeToTerms"
checked={formData.agreeToTerms}
onChange={handleChange}
className={errors.agreeToTerms ? 'error' : ''}
/>
I agree to the Terms and Conditions
</label>
{errors.agreeToTerms && <div className="error-message">{errors.agreeToTerms}</div>}
</div>
<button type="submit" className="submit-button">Register</button>
</form>
);
}
Real-world application: This is similar to a user registration form you might see on sites like GitHub, LinkedIn, or any SaaS product. It includes all the essential form handling concepts: multiple input types, validation, error display, and submission handling.
Handling Different Input Types
Different form elements require specific handling techniques. Let's explore how to handle various input types in React.
Text Inputs
// Basic text input
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
Checkboxes
// Single checkbox (boolean value)
<input
type="checkbox"
name="subscribe"
checked={formData.subscribe}
onChange={handleChange}
/>
// For the handleChange function:
const handleChange = (e) => {
const { name, checked, type } = e.target;
if (type === 'checkbox') {
setFormData({
...formData,
[name]: checked
});
} else {
// Handle other input types
}
};
Radio Buttons
// Radio button group
<div>
<label>
<input
type="radio"
name="gender"
value="male"
checked={formData.gender === 'male'}
onChange={handleChange}
/>
Male
</label>
<label>
<input
type="radio"
name="gender"
value="female"
checked={formData.gender === 'female'}
onChange={handleChange}
/>
Female
</label>
<label>
<input
type="radio"
name="gender"
value="other"
checked={formData.gender === 'other'}
onChange={handleChange}
/>
Other
</label>
</div>
Select Dropdowns
// Select dropdown
<select
name="country"
value={formData.country}
onChange={handleChange}
>
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="uk">United Kingdom</option>
<option value="au">Australia</option>
</select>
Multi-select
// Multiple select
<select
name="languages"
multiple
value={formData.languages}
onChange={(e) => {
const values = Array.from(
e.target.selectedOptions,
option => option.value
);
setFormData({
...formData,
languages: values
});
}}
>
<option value="javascript">JavaScript</option>
<option value="python">Python</option>
<option value="java">Java</option>
<option value="csharp">C#</option>
</select>
File Uploads
File inputs are typically handled as uncontrolled components because their values are read-only.
// File input example
function FileUploadForm() {
const fileInputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
const file = fileInputRef.current.files[0];
if (file) {
// Create FormData for API submission
const formData = new FormData();
formData.append('file', file);
// Submit to API
console.log('Uploading file:', file.name);
// In a real app: await fetch('/api/upload', { method: 'POST', body: formData });
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="file"
ref={fileInputRef}
/>
<button type="submit">Upload</button>
</form>
);
}
Range Sliders
// Range slider
<div>
<label htmlFor="price-range">Price Range: ${formData.priceRange}</label>
<input
type="range"
id="price-range"
name="priceRange"
min="10"
max="1000"
step="10"
value={formData.priceRange}
onChange={handleChange}
/>
</div>
Form Validation Strategies
Validation ensures that user input meets the required criteria before processing. React gives us flexible options for validating forms.
Client-side Validation Timing
- On Change: Validate as the user types (immediate feedback)
- On Blur: Validate when a field loses focus (less intrusive)
- On Submit: Validate when the form is submitted (least intrusive, but delayed feedback)
Validation on Blur Example
import React, { useState } from 'react';
function FormWithBlurValidation() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched({
...touched,
[name]: true
});
validateField(name, formData[name]);
};
const validateField = (name, value) => {
let error = null;
switch (name) {
case 'email':
if (!value) {
error = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(value)) {
error = 'Email is invalid';
}
break;
case 'password':
if (!value) {
error = 'Password is required';
} else if (value.length < 8) {
error = 'Password must be at least 8 characters';
}
break;
default:
break;
}
setErrors(prev => ({
...prev,
[name]: error
}));
return error === null;
};
const handleSubmit = (e) => {
e.preventDefault();
// Mark all fields as touched
const allTouched = Object.keys(formData).reduce(
(acc, key) => ({ ...acc, [key]: true }),
{}
);
setTouched(allTouched);
// Validate all fields
let isValid = true;
Object.keys(formData).forEach(name => {
if (!validateField(name, formData[name])) {
isValid = false;
}
});
if (isValid) {
console.log('Form is valid, submitting:', formData);
// Process the form data
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
className={touched.email && errors.email ? 'error' : ''}
/>
{touched.email && errors.email && (
<div className="error-message">{errors.email}</div>
)}
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
className={touched.password && errors.password ? 'error' : ''}
/>
{touched.password && errors.password && (
<div className="error-message">{errors.password}</div>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}
Real-world application: This blur validation approach is used in many production forms, such as login forms on banking websites or social media platforms. It provides feedback at an appropriate time without overwhelming the user during input.
Custom Validation Hooks
Creating a custom validation hook can make your form validation reusable across components.
// useFormValidation.js
import { useState, useEffect } from 'react';
const useFormValidation = (initialState, validate) => {
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// Validate when values change and form is submitting
useEffect(() => {
if (isSubmitting) {
const noErrors = Object.keys(errors).length === 0;
if (noErrors) {
// Form can be submitted
console.log('Form is valid, ready to submit', values);
setIsSubmitting(false);
} else {
setIsSubmitting(false);
}
}
}, [errors, isSubmitting, values]);
// Run validation
const validateValues = () => {
const validationErrors = validate(values);
setErrors(validationErrors);
return Object.keys(validationErrors).length === 0;
};
// Handle input changes
const handleChange = (event) => {
const { name, value, type, checked } = event.target;
setValues({
...values,
[name]: type === 'checkbox' ? checked : value
});
};
// Handle blur events
const handleBlur = (event) => {
const { name } = event.target;
setTouched({
...touched,
[name]: true
});
// Validate the field on blur
const validationErrors = validate({
...values
});
setErrors(validationErrors);
};
// Handle form submission
const handleSubmit = (event) => {
event.preventDefault();
// Mark all fields as touched
const allTouched = Object.keys(values).reduce(
(acc, key) => ({ ...acc, [key]: true }),
{}
);
setTouched(allTouched);
// Validate all fields
const validationErrors = validate(values);
setErrors(validationErrors);
// Set submitting to true only if no errors
if (Object.keys(validationErrors).length === 0) {
setIsSubmitting(true);
}
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting
};
};
export default useFormValidation;
Using the custom hook:
import React from 'react';
import useFormValidation from './useFormValidation';
// Define validation rules
const validateLoginForm = (values) => {
let errors = {};
// Email validation
if (!values.email) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = 'Email is invalid';
}
// Password validation
if (!values.password) {
errors.password = 'Password is required';
} else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
return errors;
};
function LoginForm() {
const initialState = {
email: '',
password: ''
};
const {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting
} = useFormValidation(initialState, validateLoginForm);
// Log in the user
const loginUser = () => {
console.log('Logging in with:', values);
// API call would go here
};
// When validation passes in the hook, it sets isSubmitting to true
React.useEffect(() => {
if (isSubmitting) {
loginUser();
}
}, [isSubmitting]);
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
className={touched.email && errors.email ? 'error' : ''}
/>
{touched.email && errors.email && (
<div className="error-message">{errors.email}</div>
)}
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
name="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
className={touched.password && errors.password ? 'error' : ''}
/>
{touched.password && errors.password && (
<div className="error-message">{errors.password}</div>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Log In'}
</button>
</form>
);
}
Real-world application: Many production-grade React applications use custom hooks for form validation. This approach is similar to how Formik (a popular form library) handles validation under the hood.
Form Libraries
While building forms from scratch gives you complete control, form libraries can significantly reduce boilerplate and provide battle-tested solutions.
Popular React Form Libraries
- Formik: One of the most popular form libraries for React. It handles form state, validation, error messages, and submission.
- React Hook Form: A performant, flexible and extensible form library with easy-to-use validation.
- Redux Form: For applications already using Redux, this library integrates form state with the Redux store.
- Final Form: A framework-agnostic form library that works well with React.
Formik Example
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
// Validation schema using Yup
const SignupSchema = Yup.object().shape({
firstName: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Required'),
lastName: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Required'),
email: Yup.string()
.email('Invalid email')
.required('Required'),
password: Yup.string()
.min(8, 'Password must be at least 8 characters')
.required('Required')
});
function SignupForm() {
return (
<div>
<h1>Sign Up</h1>
<Formik
initialValues={{
firstName: '',
lastName: '',
email: '',
password: ''
}}
validationSchema={SignupSchema}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({ isSubmitting }) => (
<Form>
<div>
<label htmlFor="firstName">First Name</label>
<Field name="firstName" type="text" />
<ErrorMessage name="firstName" component="div" className="error" />
</div>
<div>
<label htmlFor="lastName">Last Name</label>
<Field name="lastName" type="text" />
<ErrorMessage name="lastName" component="div" className="error" />
</div>
<div>
<label htmlFor="email">Email</label>
<Field name="email" type="email" />
<ErrorMessage name="email" component="div" className="error" />
</div>
<div>
<label htmlFor="password">Password</label>
<Field name="password" type="password" />
<ErrorMessage name="password" component="div" className="error" />
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</Form>
)}
</Formik>
</div>
);
}
React Hook Form Example
import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
// Define validation schema
const schema = yup.object().shape({
username: yup.string().required('Username is required'),
email: yup.string().email('Must be a valid email').required('Email is required'),
password: yup.string().min(8, 'Password must be at least 8 characters').required('Password is required')
});
function HookFormExample() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({
resolver: yupResolver(schema)
});
const onSubmit = data => {
console.log(data);
// API call would go here
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="username">Username</label>
<input id="username" {...register('username')} />
{errors.username && <p className="error">{errors.username.message}</p>}
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" {...register('email')} />
{errors.email && <p className="error">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" type="password" {...register('password')} />
{errors.password && <p className="error">{errors.password.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Choosing a form library: The choice between hand-rolling your own forms and using a library depends on your project's complexity:
- Simple forms: Basic React state is often sufficient.
- Complex forms: Consider a library like Formik or React Hook Form to avoid reinventing the wheel.
- Enterprise applications: A consistent form library across the codebase provides standardization and reduces development time.
Dynamic Forms
Many real-world forms need to be dynamic, with fields that can be added, removed, or changed based on user input.
Example: Dynamic Field Addition/Removal
import React, { useState } from 'react';
function DynamicForm() {
const [formValues, setFormValues] = useState({
firstName: '',
lastName: '',
email: '',
phoneNumbers: [{ number: '' }]
});
// Handle changes for basic fields
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({
...formValues,
[name]: value
});
};
// Handle phone number changes
const handlePhoneChange = (index, e) => {
const { value } = e.target;
const updatedPhoneNumbers = [...formValues.phoneNumbers];
updatedPhoneNumbers[index] = { number: value };
setFormValues({
...formValues,
phoneNumbers: updatedPhoneNumbers
});
};
// Add a new phone number field
const addPhoneNumber = () => {
setFormValues({
...formValues,
phoneNumbers: [...formValues.phoneNumbers, { number: '' }]
});
};
// Remove a phone number field
const removePhoneNumber = (index) => {
const updatedPhoneNumbers = [...formValues.phoneNumbers];
updatedPhoneNumbers.splice(index, 1);
setFormValues({
...formValues,
phoneNumbers: updatedPhoneNumbers
});
};
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formValues);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="firstName">First Name</label>
<input
type="text"
id="firstName"
name="firstName"
value={formValues.firstName}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="lastName">Last Name</label>
<input
type="text"
id="lastName"
name="lastName"
value={formValues.lastName}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formValues.email}
onChange={handleChange}
/>
</div>
<div>
<label>Phone Numbers</label>
{formValues.phoneNumbers.map((phone, index) => (
<div key={index} className="phone-input-group">
<input
type="tel"
value={phone.number}
onChange={(e) => handlePhoneChange(index, e)}
placeholder="Phone Number"
/>
{formValues.phoneNumbers.length > 1 && (
<button
type="button"
onClick={() => removePhoneNumber(index)}
className="remove-button"
>
Remove
</button>
)}
</div>
))}
<button
type="button"
onClick={addPhoneNumber}
className="add-button"
>
Add Phone Number
</button>
</div>
<button type="submit">Submit</button>
</form>
);
}
Real-world application: Dynamic forms are common in many enterprise applications. For example, an e-commerce website might allow users to add multiple shipping addresses, or a project management tool might let users assign multiple team members to a task.
Conditional Form Fields
import React, { useState } from 'react';
function ConditionalForm() {
const [formData, setFormData] = useState({
accountType: 'personal',
firstName: '',
lastName: '',
email: '',
companyName: '',
taxId: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
};
const isBusinessAccount = formData.accountType === 'business';
return (
<form onSubmit={handleSubmit}>
<div>
<label>Account Type</label>
<select
name="accountType"
value={formData.accountType}
onChange={handleChange}
>
<option value="personal">Personal</option>
<option value="business">Business</option>
</select>
</div>
{/* These fields show for both account types */}
<div>
<label htmlFor="firstName">First Name</label>
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="lastName">Last Name</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
{/* These fields only show for business accounts */}
{isBusinessAccount && (
<div className="business-fields">
<div>
<label htmlFor="companyName">Company Name</label>
<input
type="text"
id="companyName"
name="companyName"
value={formData.companyName}
onChange={handleChange}
/>
</div>
<div>
<label htmlFor="taxId">Tax ID</label>
<input
type="text"
id="taxId"
name="taxId"
value={formData.taxId}
onChange={handleChange}
/>
</div>
</div>
)}
<button type="submit">Create Account</button>
</form>
);
}
Real-world application: Conditional forms are used in many signup flows, like when creating accounts on platforms that serve both individuals and businesses (e.g., payment processors like Stripe or Paypal, or accounting software like QuickBooks).
Form Accessibility Considerations
Creating accessible forms ensures that all users, including those with disabilities, can interact with your application.
Key Accessibility Guidelines
- Proper Labeling: Use
<label>elements properly associated with form controls. - Semantic HTML: Use appropriate form elements (
<form>,<fieldset>,<legend>). - Tab Navigation: Ensure logical tab order through the form.
- Error Messages: Provide clear error messages associated with form controls.
- ARIA Attributes: Use ARIA attributes when necessary to enhance accessibility.
- Keyboard Navigation: Ensure all form controls are usable via keyboard.
Accessible Form Example
import React, { useState } from 'react';
function AccessibleForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
topic: '',
message: '',
subscribe: false
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
// Clear errors when field is updated
if (errors[name]) {
setErrors({
...errors,
[name]: null
});
}
};
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.topic) {
newErrors.topic = 'Please select a topic';
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('Form submitted:', formData);
// Submit form data
alert('Message sent successfully!');
} else {
// Set focus to the first field with an error
const firstErrorField = Object.keys(errors)[0];
if (firstErrorField && document.getElementById(firstErrorField)) {
document.getElementById(firstErrorField).focus();
}
}
};
return (
<form onSubmit={handleSubmit} noValidate>
<h2 id="contactFormHeading">Contact Us</h2>
<p>Fields marked with * are required</p>
<div className="form-group">
<label htmlFor="name" id="name-label">
Name *
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
aria-required="true"
aria-describedby={errors.name ? "name-error" : null}
aria-invalid={errors.name ? "true" : "false"}
/>
{errors.name && (
<div className="error-message" id="name-error" role="alert">
{errors.name}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="email" id="email-label">
Email *
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
aria-required="true"
aria-describedby={errors.email ? "email-error" : null}
aria-invalid={errors.email ? "true" : "false"}
/>
{errors.email && (
<div className="error-message" id="email-error" role="alert">
{errors.email}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="topic" id="topic-label">
Topic *
</label>
<select
id="topic"
name="topic"
value={formData.topic}
onChange={handleChange}
aria-required="true"
aria-describedby={errors.topic ? "topic-error" : null}
aria-invalid={errors.topic ? "true" : "false"}
>
<option value="">Please select a topic</option>
<option value="general">General Inquiry</option>
<option value="support">Technical Support</option>
<option value="billing">Billing Question</option>
</select>
{errors.topic && (
<div className="error-message" id="topic-error" role="alert">
{errors.topic}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="message" id="message-label">
Message *
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows="5"
aria-required="true"
aria-describedby={errors.message ? "message-error" : null}
aria-invalid={errors.message ? "true" : "false"}
/>
{errors.message && (
<div className="error-message" id="message-error" role="alert">
{errors.message}
</div>
)}
</div>
<div className="form-group checkbox-group">
<input
type="checkbox"
id="subscribe"
name="subscribe"
checked={formData.subscribe}
onChange={handleChange}
/>
<label htmlFor="subscribe">
Subscribe to our newsletter
</label>
</div>
<button type="submit">Send Message</button>
</form>
);
}
Real-world application: Government websites, educational institutions, and many corporate sites are required to meet accessibility standards (like WCAG) for their forms. Accessible forms also benefit all users by providing a better user experience.
Form Submission and API Integration
Handling form submission often involves sending data to an API and managing loading/success/error states.
Complete Form with API Submission
import React, { useState } from 'react';
import axios from 'axios';
function SubscriptionForm() {
const [formData, setFormData] = useState({
fullName: '',
email: '',
plan: 'basic'
});
const [formState, setFormState] = useState({
isSubmitting: false,
isSubmitted: false,
error: null
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setFormState({
isSubmitting: true,
isSubmitted: false,
error: null
});
try {
// Submit form data to API
const response = await axios.post('https://api.example.com/subscribe', formData);
// Handle successful submission
setFormState({
isSubmitting: false,
isSubmitted: true,
error: null
});
// Clear form
setFormData({
fullName: '',
email: '',
plan: 'basic'
});
console.log('Subscription created:', response.data);
} catch (error) {
// Handle error
setFormState({
isSubmitting: false,
isSubmitted: false,
error: error.response?.data?.message || 'An error occurred. Please try again.'
});
console.error('Submission error:', error);
}
};
// Show success message after submission
if (formState.isSubmitted) {
return (
<div className="success-message">
<h2>Subscription Successful!</h2>
<p>Thank you for subscribing to our service.</p>
<button
onClick={() => setFormState({ isSubmitting: false, isSubmitted: false, error: null })}
>
Subscribe Another
</button>
</div>
);
}
return (
<div>
<h2>Subscribe to Our Service</h2>
{formState.error && (
<div className="error-banner" role="alert">
{formState.error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="fullName">Full Name</label>
<input
type="text"
id="fullName"
name="fullName"
value={formData.fullName}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="plan">Subscription Plan</label>
<select
id="plan"
name="plan"
value={formData.plan}
onChange={handleChange}
>
<option value="basic">Basic - $9.99/month</option>
<option value="pro">Pro - $19.99/month</option>
<option value="enterprise">Enterprise - $49.99/month</option>
</select>
</div>
<button
type="submit"
disabled={formState.isSubmitting}
className={formState.isSubmitting ? 'submitting' : ''}
>
{formState.isSubmitting ? 'Subscribing...' : 'Subscribe Now'}
</button>
</form>
</div>
);
}
Real-world application: This pattern is used in nearly every subscription-based service, from streaming platforms to SaaS products, where user information needs to be collected and processed through an API.
Practice Activities
Activity 1: Login Form with Validation
Create a login form with email and password fields
Create a login form with email and password fields. Add client-side validation to ensure:
- Email field contains a valid email format
- Password field is at least 8 characters long
- Show appropriate error messages
- Disable the submit button when the form is invalid
Add appropriate loading states during form submission and success/error messages after submission.
Activity 2: Multi-step Form
Create a multi-step form with the following steps:
- Personal Information (name, email, phone)
- Address Information (street, city, state, zip)
- Account Setup (username, password, confirm password)
- Review and Submit
Include navigation buttons to move between steps, validate each step before proceeding, and display a summary of all information before final submission.
Activity 3: Dynamic Form Builder
Create a simple form builder that allows users to:
- Add different types of form fields (text, email, select, checkbox)
- Remove fields
- Reorder fields by drag and drop or up/down buttons
- Preview the form they're building
Bonus: Allow users to save and load form templates.
Activity 4: Form with File Upload
Create a profile update form that includes:
- Basic text fields for profile information
- A file input for profile picture upload
- Image preview after selection
- Validation for file type (only images) and size (max 2MB)
Mock the API submission and handle the form data appropriately.
Summary
- Controlled vs. Uncontrolled Components: Understand the differences and when to use each approach in your forms.
- Form State Management: Use React state to track form values, validation, and submission status.
- Validation Strategies: Implement validation at appropriate times (on change, blur, or submit) for the best user experience.
- Different Input Types: Handle various form elements like text inputs, checkboxes, radio buttons, select dropdowns, and file uploads.
- Dynamic Forms: Create forms that adapt based on user input, with conditional fields and dynamic field arrays.
- Form Libraries: Know when to use libraries like Formik or React Hook Form for complex form scenarios.
- Accessibility: Ensure forms are accessible with proper labeling, ARIA attributes, and keyboard navigation.
- API Integration: Effectively handle form submission to APIs with appropriate loading and error states.
Form handling is a fundamental skill for React developers. By mastering the concepts covered in this lecture, you'll be able to create intuitive, accessible, and functional forms that provide an excellent user experience.