Introduction to Forms in React
Forms are a fundamental part of web applications, allowing users to input and submit data. In React, forms are slightly different from traditional HTML forms because the data is typically controlled by React components rather than the DOM itself.
Real-world analogy: Think of a React form like a digital survey with an assistant. In a paper survey, you fill in answers and submit the whole thing at once. With the digital survey, the assistant (React) watches what you type in real-time, can validate your answers immediately, and can even change questions based on previous answers.
Key Concepts in React Forms
- Controlled Components: Form elements whose values are controlled by React state
- Uncontrolled Components: Form elements that maintain their own state internally
- Form Submission: Handling form submission events in React
- Form Validation: Validating user input before submission
- Form State Management: Organizing and managing complex form state
Controlled Components
In HTML, form elements like <input>, <textarea>, and <select>
maintain their own state and update based on user input. In React, we typically want to have all state managed by React.
A controlled component is a form element whose value is controlled by React state. Every state change is handled through a handler function, giving you complete control over the form.
Basic Controlled Input
import React, { useState } from 'react';
function NameInput() {
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">Greet Me</button>
</form>
);
}
In this example:
- The
namestate stores the current input value - We set the input's
valueattribute to thenamestate - The
onChangehandler updates the state when the user types - The form submission is handled by
handleSubmit
Working with Different Form Elements
Text and Textarea Inputs
Both <input type="text"> and <textarea> work similarly in React:
function TextForm() {
const [inputValue, setInputValue] = useState('');
const [textareaValue, setTextareaValue] = useState('');
return (
<form>
<div>
<label htmlFor="simple-input">Simple Input:</label>
<input
id="simple-input"
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
<div>
<label htmlFor="textarea-input">Textarea:</label>
<textarea
id="textarea-input"
value={textareaValue}
onChange={(e) => setTextareaValue(e.target.value)}
rows="4"
/>
</div>
<div>
<p>Input value: {inputValue}</p>
<p>Textarea value: {textareaValue}</p>
</div>
</form>
);
}
Note: In HTML, a <textarea> defines its content between tags.
In React, a <textarea> uses a value attribute instead, making it
work more consistently with other form elements.
Checkbox and Radio Inputs
For checkboxes and radio buttons, we use the checked attribute instead of value:
function PreferenceForm() {
const [isSubscribed, setIsSubscribed] = useState(false);
const [plan, setPlan] = useState('free');
return (
<form>
<div>
<label>
<input
type="checkbox"
checked={isSubscribed}
onChange={(e) => setIsSubscribed(e.target.checked)}
/>
Subscribe to newsletter
</label>
</div>
<div>
<p>Subscription Plan:</p>
<label>
<input
type="radio"
value="free"
checked={plan === 'free'}
onChange={(e) => setPlan(e.target.value)}
/>
Free
</label>
<label>
<input
type="radio"
value="pro"
checked={plan === 'pro'}
onChange={(e) => setPlan(e.target.value)}
/>
Pro
</label>
<label>
<input
type="radio"
value="enterprise"
checked={plan === 'enterprise'}
onChange={(e) => setPlan(e.target.value)}
/>
Enterprise
</label>
</div>
<div>
<p>You are {isSubscribed ? 'subscribed' : 'not subscribed'} to the newsletter.</p>
<p>Your plan: {plan}</p>
</div>
</form>
);
}
Select Dropdowns
The <select> element also works as a controlled component in React:
function CountrySelector() {
const [country, setCountry] = useState('');
return (
<form>
<label htmlFor="country">Select your country:</label>
<select
id="country"
value={country}
onChange={(e) => setCountry(e.target.value)}
>
<option value="">-- Select a country --</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="mx">Mexico</option>
<option value="uk">United Kingdom</option>
<option value="fr">France</option>
</select>
{country && (
<p>You selected: {country}</p>
)}
</form>
);
}
Multiple Select
For a <select> with multiple attribute, we handle an array of values:
function SkillsSelector() {
const [skills, setSkills] = useState([]);
const handleSkillChange = (event) => {
const selectedOptions = Array.from(event.target.selectedOptions,
option => option.value);
setSkills(selectedOptions);
};
return (
<form>
<label htmlFor="skills">Select your skills (hold Ctrl/Cmd to select multiple):</label>
<select
id="skills"
multiple
value={skills}
onChange={handleSkillChange}
style={{ height: '120px' }}
>
<option value="javascript">JavaScript</option>
<option value="react">React</option>
<option value="nodejs">Node.js</option>
<option value="python">Python</option>
<option value="java">Java</option>
<option value="csharp">C#</option>
</select>
{skills.length > 0 && (
<div>
<p>Your skills:</p>
<ul>
{skills.map(skill => (
<li key={skill}>{skill}</li>
))}
</ul>
</div>
)}
</form>
);
}
Handling Multiple Form Fields
For forms with multiple fields, managing separate state for each field can be cumbersome. Instead, we can use a single state object to handle all form fields:
function RegistrationForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
age: '',
gender: '',
interests: []
});
const handleChange = (event) => {
const { name, value, type, checked } = event.target;
if (type === 'checkbox') {
// Handle checkbox (interests)
if (checked) {
setFormData({
...formData,
interests: [...formData.interests, value]
});
} else {
setFormData({
...formData,
interests: formData.interests.filter(interest => interest !== value)
});
}
} else {
// Handle all other input types
setFormData({
...formData,
[name]: value
});
}
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('Form submitted:', formData);
// Here you would typically send the data to a server
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="firstName">First Name:</label>
<input
id="firstName"
name="firstName"
type="text"
value={formData.firstName}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="lastName">Last Name:</label>
<input
id="lastName"
name="lastName"
type="text"
value={formData.lastName}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
required
minLength="8"
/>
</div>
<div>
<label htmlFor="age">Age:</label>
<input
id="age"
name="age"
type="number"
value={formData.age}
onChange={handleChange}
min="18"
max="120"
/>
</div>
<div>
<p>Gender:</p>
<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>
<div>
<p>Interests:</p>
<label>
<input
type="checkbox"
name="interests"
value="technology"
checked={formData.interests.includes('technology')}
onChange={handleChange}
/>
Technology
</label>
<label>
<input
type="checkbox"
name="interests"
value="sports"
checked={formData.interests.includes('sports')}
onChange={handleChange}
/>
Sports
</label>
<label>
<input
type="checkbox"
name="interests"
value="music"
checked={formData.interests.includes('music')}
onChange={handleChange}
/>
Music
</label>
<label>
<input
type="checkbox"
name="interests"
value="art"
checked={formData.interests.includes('art')}
onChange={handleChange}
/>
Art
</label>
</div>
<button type="submit">Register</button>
</form>
);
}
Key benefits of this approach:
- Single
handleChangefunction for all inputs - All form data stored in a structured object
- Easy to add, remove, or modify form fields
- Convenient for form submission
Note: The key to making this work is using the name attribute
on each input that matches the corresponding property in your state object.
Form Validation
Form validation is crucial for ensuring users submit correct and complete data. There are several approaches to validation in React forms:
Built-in HTML5 Validation
Modern browsers provide built-in validation using attributes like required,
min, max, pattern, etc:
function HTML5ValidationForm() {
const handleSubmit = (event) => {
event.preventDefault();
// Form will only submit if all validations pass
alert('Form submitted successfully!');
};
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor="username">Username (3-20 characters):</label>
<input
id="username"
type="text"
required
minLength="3"
maxLength="20"
pattern="[A-Za-z0-9]+"
title="Username can only contain letters and numbers"
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
required
/>
</div>
<div>
<label htmlFor="age">Age (18+):</label>
<input
id="age"
type="number"
required
min="18"
max="120"
/>
</div>
<div>
<label htmlFor="website">Website:</label>
<input
id="website"
type="url"
placeholder="https://example.com"
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
Note: The noValidate attribute lets you disable the browser's default validation UI
while still being able to use the validation API programmatically.
Custom Validation in React
For more complex validation logic, or for a consistent UI across browsers, you can implement custom validation in React:
function CustomValidationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (event) => {
const { name, value } = event.target;
setFormData({
...formData,
[name]: value
});
};
const handleBlur = (event) => {
const { name } = event.target;
setTouched({
...touched,
[name]: true
});
// Validate the field on blur
validateField(name, formData[name]);
};
const validateField = (name, value) => {
let newErrors = { ...errors };
switch (name) {
case 'username':
if (!value) {
newErrors.username = 'Username is required';
} else if (value.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
} else if (!/^[A-Za-z0-9]+$/.test(value)) {
newErrors.username = 'Username can only contain letters and numbers';
} else {
delete newErrors.username;
}
break;
case 'email':
if (!value) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(value)) {
newErrors.email = 'Email address is invalid';
} else {
delete newErrors.email;
}
break;
case 'password':
if (!value) {
newErrors.password = 'Password is required';
} else if (value.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
} else if (!/[A-Z]/.test(value)) {
newErrors.password = 'Password must contain at least one uppercase letter';
} else if (!/[0-9]/.test(value)) {
newErrors.password = 'Password must contain at least one number';
} else {
delete newErrors.password;
}
// Also validate confirmPassword if it exists
if (formData.confirmPassword && value !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
} else if (formData.confirmPassword) {
delete newErrors.confirmPassword;
}
break;
case 'confirmPassword':
if (!value) {
newErrors.confirmPassword = 'Please confirm your password';
} else if (value !== formData.password) {
newErrors.confirmPassword = 'Passwords do not match';
} else {
delete newErrors.confirmPassword;
}
break;
default:
break;
}
setErrors(newErrors);
};
const validateForm = () => {
// Validate all fields
Object.keys(formData).forEach(field => {
validateField(field, formData[field]);
});
// Mark all fields as touched
const allTouched = Object.keys(formData).reduce((acc, field) => {
acc[field] = true;
return acc;
}, {});
setTouched(allTouched);
// Form is valid if there are no errors
return Object.keys(errors).length === 0;
};
const handleSubmit = (event) => {
event.preventDefault();
if (validateForm()) {
// Form is valid, proceed with submission
console.log('Form data:', formData);
alert('Form submitted successfully!');
} else {
console.log('Form has errors:', errors);
}
};
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor="username">Username:</label>
<input
id="username"
name="username"
type="text"
value={formData.username}
onChange={handleChange}
onBlur={handleBlur}
className={touched.username && errors.username ? 'error' : ''}
/>
{touched.username && errors.username && (
<div className="error-message">{errors.username}</div>
)}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="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
id="password"
name="password"
type="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>
<div>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
className={touched.confirmPassword && errors.confirmPassword ? 'error' : ''}
/>
{touched.confirmPassword && errors.confirmPassword && (
<div className="error-message">{errors.confirmPassword}</div>
)}
</div>
<button type="submit">Register</button>
</form>
);
}
Key features of this validation approach:
- Validates each field individually with specific rules
- Tracks which fields have been "touched" to avoid showing errors prematurely
- Shows error messages next to the relevant fields
- Performs full validation on form submission
- Applies CSS classes for styling error states
Uncontrolled Components
While controlled components are the recommended way to handle form data in React, there are cases where using uncontrolled components can be simpler.
An uncontrolled component lets the DOM handle the form data internally, and you extract the value when needed (e.g., on form submission) using refs.
import React, { useRef } from 'react';
function UncontrolledForm() {
const nameRef = useRef();
const emailRef = useRef();
const messageRef = useRef();
const handleSubmit = (event) => {
event.preventDefault();
// Access form values using refs
const formData = {
name: nameRef.current.value,
email: emailRef.current.value,
message: messageRef.current.value
};
console.log('Form submitted:', formData);
// Reset the form
event.target.reset();
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
type="text"
ref={nameRef}
defaultValue=""
required
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
ref={emailRef}
defaultValue=""
required
/>
</div>
<div>
<label htmlFor="message">Message:</label>
<textarea
id="message"
ref={messageRef}
defaultValue=""
rows="4"
required
/>
</div>
<button type="submit">Send Message</button>
</form>
);
}
When to Use Uncontrolled Components
- For simple forms where real-time validation isn't needed
- When integrating with non-React code or third-party libraries
- For file inputs (which are inherently uncontrolled)
- When performance is a concern (for forms with many fields)
File Inputs
File inputs are always uncontrolled in React, since the value is read-only:
function FileUploadForm() {
const fileInputRef = useRef();
const handleSubmit = (event) => {
event.preventDefault();
const files = fileInputRef.current.files;
console.log('Selected files:', files);
// To upload files, you would typically use FormData
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
// Then send formData to your server
console.log('Ready to upload:', formData);
};
return (
<form onSubmit={handleSubmit} >
<div>
<label htmlFor="files">Select files:</label>
<input
id="files"
type="file"
ref={fileInputRef}
multiple
/>
</div>
<button type="submit">Upload</button>
</form>
);
}
Form Libraries
For complex forms, using a form library can save time and reduce boilerplate. Here are some popular options:
Formik
Formik is one of the most popular form libraries for React, offering a complete solution:
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup'; // For validation schema
function FormikExample() {
// Define validation schema
const validationSchema = Yup.object({
firstName: Yup.string()
.max(15, 'Must be 15 characters or less')
.required('Required'),
lastName: Yup.string()
.max(20, 'Must be 20 characters or less')
.required('Required'),
email: Yup.string()
.email('Invalid email address')
.required('Required'),
acceptedTerms: Yup.boolean()
.required('Required')
.oneOf([true], 'You must accept the terms and conditions'),
jobType: Yup.string()
.oneOf(
['designer', 'developer', 'manager', 'other'],
'Invalid job type'
)
.required('Required')
});
return (
<div>
<h2>Sign Up</h2>
<Formik
initialValues={{
firstName: '',
lastName: '',
email: '',
acceptedTerms: false,
jobType: ''
}}
validationSchema={validationSchema}
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="jobType">Job Type</label>
<Field name="jobType" as="select">
<option value="">Select a job type</option>
<option value="designer">Designer</option>
<option value="developer">Developer</option>
<option value="manager">Product Manager</option>
<option value="other">Other</option>
</Field>
<ErrorMessage name="jobType" component="div" className="error" />
</div>
<div>
<label>
<Field name="acceptedTerms" type="checkbox" />
I accept the terms and conditions
</label>
<ErrorMessage name="acceptedTerms" component="div" className="error" />
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</Form>
)}
</Formik>
</div>
);
}
React Hook Form
React Hook Form is a lightweight library with a focus on performance:
import { useForm } from 'react-hook-form';
function HookFormExample() {
const {
register,
handleSubmit,
watch,
formState: { errors, isSubmitting }
} = useForm();
const onSubmit = async (data) => {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(data);
alert('Form submitted!');
};
// Watch the password field for confirmation validation
const password = watch('password');
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
{...register('name', {
required: 'Name is required',
minLength: {
value: 2,
message: 'Name must be at least 2 characters'
}
})}
/>
{errors.name && (
<p className="error">{errors.name.message}</p>
)}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
{...register('email', {
required: 'Email is required',
pattern: {
value: /\S+@\S+\.\S+/,
message: 'Email address is invalid'
}
})}
/>
{errors.email && (
<p className="error">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
{...register('password', {
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must be at least 8 characters'
}
})}
/>
{errors.password && (
<p className="error">{errors.password.message}</p>
)}
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
id="confirmPassword"
type="password"
{...register('confirmPassword', {
required: 'Please confirm your password',
validate: value =>
value === password || 'Passwords do not match'
})}
/>
{errors.confirmPassword && (
<p className="error">{errors.confirmPassword.message}</p>
)}
</div>
<div>
<label htmlFor="age">Age:</label>
<input
id="age"
type="number"
{...register('age', {
required: 'Age is required',
min: {
value: 18,
message: 'You must be at least 18 years old'
},
max: {
value: 120,
message: 'Age cannot exceed 120'
}
})}
/>
{errors.age && (
<p className="error">{errors.age.message}</p>
)}
</div>
<div>
<label htmlFor="occupation">Occupation:</label>
<select
id="occupation"
{...register('occupation', {
required: 'Please select an occupation'
})}
>
<option value="">Select your occupation</option>
<option value="developer">Developer</option>
<option value="designer">Designer</option>
<option value="manager">Manager</option>
<option value="student">Student</option>
<option value="other">Other</option>
</select>
{errors.occupation && (
<p className="error">{errors.occupation.message}</p>
)}
</div>
<div>
<label>
<input
type="checkbox"
{...register('termsAccepted', {
required: 'You must accept the terms and conditions'
})}
/>
I accept the terms and conditions
</label>
{errors.termsAccepted && (
<p className="error">{errors.termsAccepted.message}</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Comparing Form Libraries
| Feature | Formik | React Hook Form | Redux Form |
|---|---|---|---|
| Bundle Size | Medium | Small | Large |
| Performance | Good | Excellent | Average |
| Learning Curve | Medium | Low | High |
| Form State Management | Internal | Uncontrolled | Redux |
| Validation | Built-in or Yup | Built-in or Yup | Custom |
Real-World Applications
Dynamic Form with Field Array
A common requirement is to handle dynamic form fields, like a list of items:
function DynamicFormExample() {
const [formValues, setFormValues] = useState({
projectName: '',
tasks: [{ title: '', description: '', completed: false }]
});
const handleChange = (event) => {
const { name, value } = event.target;
setFormValues(prev => ({
...prev,
[name]: value
}));
};
const handleTaskChange = (index, event) => {
const { name, value, type, checked } = event.target;
const newValue = type === 'checkbox' ? checked : value;
const tasks = [...formValues.tasks];
tasks[index] = {
...tasks[index],
[name]: newValue
};
setFormValues(prev => ({
...prev,
tasks
}));
};
const handleAddTask = () => {
setFormValues(prev => ({
...prev,
tasks: [...prev.tasks, { title: '', description: '', completed: false }]
}));
};
const handleRemoveTask = (index) => {
const tasks = [...formValues.tasks];
tasks.splice(index, 1);
setFormValues(prev => ({
...prev,
tasks
}));
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('Project data:', formValues);
alert('Project saved!');
};
return (
<form onSubmit={handleSubmit}>
<h2>Project Details</h2>
<div>
<label htmlFor="projectName">Project Name:</label>
<input
id="projectName"
name="projectName"
value={formValues.projectName}
onChange={handleChange}
required
/>
</div>
<h3>Tasks</h3>
{formValues.tasks.map((task, index) => (
<div key={index} className="task-item">
<h4>Task #{index + 1}</h4>
<div>
<label htmlFor={`task-title-${index}`}>Title:</label>
<input
id={`task-title-${index}`}
name="title"
value={task.title}
onChange={(e) => handleTaskChange(index, e)}
required
/>
</div>
<div>
<label htmlFor={`task-description-${index}`}>Description:</label>
<textarea
id={`task-description-${index}`}
name="description"
value={task.description}
onChange={(e) => handleTaskChange(index, e)}
rows="2"
/>
</div>
<div>
<label>
<input
type="checkbox"
name="completed"
checked={task.completed}
onChange={(e) => handleTaskChange(index, e)}
/>
Completed
</label>
</div>
{formValues.tasks.length > 1 && (
<button
type="button"
onClick={() => handleRemoveTask(index)}
className="remove-button"
>
Remove Task
</button>
)}
</div>
))}
<button
type="button"
onClick={handleAddTask}
className="add-button"
>
Add Task
</button>
<div>
<button type="submit" className="submit-button">
Save Project
</button>
</div>
</form>
);
}
Multi-Step Form with Progress Tracking
For complex forms, breaking them into multiple steps improves user experience:
function MultiStepForm() {
const [formData, setFormData] = useState({
// Step 1: Personal Info
firstName: '',
lastName: '',
email: '',
// Step 2: Address
street: '',
city: '',
state: '',
zipCode: '',
// Step 3: Account Settings
username: '',
password: '',
notifications: false
});
const [currentStep, setCurrentStep] = useState(1);
const totalSteps = 3;
const handleChange = (event) => {
const { name, value, type, checked } = event.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const nextStep = () => {
setCurrentStep(prev => Math.min(prev + 1, totalSteps));
};
const prevStep = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
};
const handleSubmit = (event) => {
event.preventDefault();
// Submit the form data
console.log('Form submitted:', formData);
alert('Registration complete!');
};
// Render different form steps
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<div className="form-step">
<h3>Personal Information</h3>
<div>
<label htmlFor="firstName">First Name:</label>
<input
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="lastName">Last Name:</label>
<input
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
</div>
);
case 2:
return (
<div className="form-step">
<h3>Address Information</h3>
<div>
<label htmlFor="street">Street Address:</label>
<input
id="street"
name="street"
value={formData.street}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="city">City:</label>
<input
id="city"
name="city"
value={formData.city}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="state">State:</label>
<select
id="state"
name="state"
value={formData.state}
onChange={handleChange}
required
>
<option value="">Select a state</option>
<option value="CA">California</option>
<option value="NY">New York</option>
<option value="TX">Texas</option>
{/* More states... */}
</select>
</div>
<div>
<label htmlFor="zipCode">ZIP Code:</label>
<input
id="zipCode"
name="zipCode"
value={formData.zipCode}
onChange={handleChange}
required
pattern="[0-9]{5}"
/>
</div>
</div>
);
case 3:
return (
<div className="form-step">
<h3>Account Settings</h3>
<div>
<label htmlFor="username">Username:</label>
<input
id="username"
name="username"
value={formData.username}
onChange={handleChange}
required
minLength="4"
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
required
minLength="8"
/>
</div>
<div>
<label>
<input
type="checkbox"
name="notifications"
checked={formData.notifications}
onChange={handleChange}
/>
Receive email notifications
</label>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="multi-step-form">
<div className="progress-bar">
{[...Array(totalSteps)].map((_, index) => (
<div
key={index}
className={`progress-step ${currentStep > index ? 'completed' : ''} ${currentStep === index + 1 ? 'active' : ''}`}
onClick={() => setCurrentStep(index + 1)}
>
{index + 1}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
{renderStep()}
<div className="form-navigation">
{currentStep > 1 && (
<button type="button" onClick={prevStep}>
Previous
</button>
)}
{currentStep < totalSteps ? (
<button type="button" onClick={nextStep} >
Next
</button>
) : (
<button type="submit" >
Submit
</button>
)}
</div>
</form>
</div>
);
}
Practice Exercises
Exercise 1: Login Form
Create a login form with the following features:
- Email and password fields with validation
- "Remember me" checkbox
- Form submission handling
- Visual feedback during submission (loading state)
- Error message display
Exercise 2: Registration Form
Build a user registration form that:
- Collects personal info (name, email, birth date)
- Validates password requirements (length, complexity)
- Confirms password matches
- Includes terms acceptance checkbox
- Shows appropriate validation errors
Exercise 3: Feedback Survey
Create a customer feedback survey form with:
- Multiple question types (text, rating, multiple choice)
- Dynamic fields based on previous answers
- Field validation
- Ability to save progress and continue later
- Submission handling with confirmation
Summary
In this lecture, we've covered:
- The difference between controlled and uncontrolled components in React
- Working with various form elements (text inputs, checkboxes, selects, etc.)
- Managing complex form state in React
- Form validation techniques (HTML5 and custom validation)
- Handling form submission
- Using form libraries for more complex scenarios
- Real-world patterns like dynamic fields and multi-step forms
Forms are a critical part of most web applications, and React provides powerful tools for creating interactive, user-friendly form experiences. By understanding these concepts, you can build forms that are both easy to use and maintainable.