Form Handling in React
Form handling is crucial for interactive applications. This article covers form patterns and best practices.
Introduction
Form handling provides:
- Controlled components
- Form validation
- Error handling
- State management
- User feedback
Understanding form handling helps you:
- Build interactive forms
- Validate user input
- Handle form submission
- Manage form state
- Improve user experience
Controlled Components
Basic Controlled Forms
import { useState } from 'react';
// โ
Good: Controlled input
function TextInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Enter text"
/>
);
}
// โ
Good: Controlled form
function LoginForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
rememberMe: false
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
<label>
<input
type="checkbox"
name="rememberMe"
checked={formData.rememberMe}
onChange={handleChange}
/>
Remember me
</label>
<button type="submit">Login</button>
</form>
);
}
// โ
Good: Select and textarea
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
country: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
return (
<form>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<select
name="country"
value={formData.country}
onChange={handleChange}
>
<option value="">Select country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
/>
</form>
);
}
Form Validation
// โ
Good: Basic validation
function RegistrationForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const validateForm = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
return newErrors;
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
const newErrors = validateForm();
if (Object.keys(newErrors).length === 0) {
console.log('Form is valid, submitting...');
} else {
setErrors(newErrors);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <p className="error">{errors.email}</p>}
</div>
<div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
{errors.password && <p className="error">{errors.password}</p>}
</div>
<div>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
placeholder="Confirm Password"
/>
{errors.confirmPassword && <p className="error">{errors.confirmPassword}</p>}
</div>
<button type="submit">Register</button>
</form>
);
}
// โ
Good: Real-time validation
function EmailInput() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const value = e.target.value;
setEmail(value);
if (!value) {
setError('Email is required');
} else if (!/\S+@\S+\.\S+/.test(value)) {
setError('Email is invalid');
} else {
setError('');
}
};
return (
<div>
<input
type="email"
value={email}
onChange={handleChange}
placeholder="Email"
/>
{error && <p className="error">{error}</p>}
</div>
);
}
React Hook Form
Basic React Hook Form
import { useForm } from 'react-hook-form';
// โ
Good: React Hook Form setup
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
defaultValues: {
email: '',
password: ''
}
});
const onSubmit = (data) => {
console.log('Form data:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /\S+@\S+\.\S+/,
message: 'Email is invalid'
}
})}
placeholder="Email"
/>
{errors.email && <p className="error">{errors.email.message}</p>}
<input
{...register('password', {
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must be at least 8 characters'
}
})}
type="password"
placeholder="Password"
/>
{errors.password && <p className="error">{errors.password.message}</p>}
<button type="submit">Login</button>
</form>
);
}
// โ
Good: Dynamic fields
function DynamicForm() {
const { register, handleSubmit, watch, formState: { errors } } = useForm();
const watchedFields = watch();
const onSubmit = (data) => {
console.log('Form data:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('firstName', { required: 'First name is required' })}
placeholder="First name"
/>
{errors.firstName && <p>{errors.firstName.message}</p>}
<input
{...register('lastName', { required: 'Last name is required' })}
placeholder="Last name"
/>
{errors.lastName && <p>{errors.lastName.message}</p>}
<p>Full name: {watchedFields.firstName} {watchedFields.lastName}</p>
<button type="submit">Submit</button>
</form>
);
}
// โ
Good: Conditional fields
function ConditionalForm() {
const { register, handleSubmit, watch, formState: { errors } } = useForm();
const accountType = watch('accountType');
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<select {...register('accountType')}>
<option value="">Select account type</option>
<option value="personal">Personal</option>
<option value="business">Business</option>
</select>
{accountType === 'business' && (
<input
{...register('companyName', { required: 'Company name is required' })}
placeholder="Company name"
/>
)}
<button type="submit">Submit</button>
</form>
);
}
Advanced React Hook Form
import { useForm, useFieldArray, Controller } from 'react-hook-form';
// โ
Good: Array fields
function MultipleEmailsForm() {
const { register, handleSubmit, control, formState: { errors } } = useForm({
defaultValues: {
emails: [{ value: '' }]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'emails'
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{fields.map((field, index) => (
<div key={field.id}>
<input
{...register(`emails.${index}.value`, {
required: 'Email is required'
})}
placeholder="Email"
/>
{errors.emails?.[index]?.value && (
<p>{errors.emails[index].value.message}</p>
)}
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ value: '' })}>
Add Email
</button>
<button type="submit">Submit</button>
</form>
);
}
// โ
Good: Custom validation
function CustomValidationForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const validateUsername = async (value) => {
const response = await fetch(`/api/check-username?username=${value}`);
const data = await response.json();
return data.available || 'Username is already taken';
};
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input
{...register('username', {
required: 'Username is required',
validate: validateUsername
})}
placeholder="Username"
/>
{errors.username && <p>{errors.username.message}</p>}
<button type="submit">Submit</button>
</form>
);
}
// โ
Good: Controller for custom components
function CustomComponentForm() {
const { control, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="date"
control={control}
render={({ field }) => (
<input type="date" {...field} />
)}
/>
<button type="submit">Submit</button>
</form>
);
}
Formik
Basic Formik Setup
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
// โ
Good: Formik with validation schema
const validationSchema = Yup.object().shape({
email: Yup.string()
.email('Invalid email')
.required('Email is required'),
password: Yup.string()
.min(8, 'Password must be at least 8 characters')
.required('Password is required')
});
function LoginForm() {
return (
<Formik
initialValues={{ email: '', password: '' }}
validationSchema={validationSchema}
onSubmit={(values) => {
console.log('Form submitted:', values);
}}
>
{({ isSubmitting }) => (
<Form>
<Field
type="email"
name="email"
placeholder="Email"
/>
<ErrorMessage name="email" component="p" />
<Field
type="password"
name="password"
placeholder="Password"
/>
<ErrorMessage name="password" component="p" />
<button type="submit" disabled={isSubmitting}>
Login
</button>
</Form>
)}
</Formik>
);
}
// โ
Good: Custom field component
function CustomField({ label, ...props }) {
return (
<div>
<label>{label}</label>
<Field {...props} />
<ErrorMessage name={props.name} component="p" />
</div>
);
}
function RegistrationForm() {
return (
<Formik
initialValues={{ name: '', email: '', password: '' }}
validationSchema={validationSchema}
onSubmit={(values) => console.log(values)}
>
<Form>
<CustomField label="Name" name="name" type="text" />
<CustomField label="Email" name="email" type="email" />
<CustomField label="Password" name="password" type="password" />
<button type="submit">Register</button>
</Form>
</Formik>
);
}
Best Practices
-
Use controlled components:
// โ Good: Controlled const [value, setValue] = useState(''); <input value={value} onChange={(e) => setValue(e.target.value)} /> // โ Bad: Uncontrolled <input defaultValue="initial" /> -
Validate on submit and change:
// โ Good: Validate on both const handleChange = (e) => { setValue(e.target.value); validateField(e.target.value); }; // โ Bad: Only on submit const handleSubmit = () => { validateForm(); }; -
Use form libraries for complex forms:
// โ Good: React Hook Form for complex forms const { register, handleSubmit } = useForm(); // โ Bad: Manual state for complex forms const [formData, setFormData] = useState({...});
Summary
Form handling is essential. Key takeaways:
- Use controlled components
- Validate user input
- Provide clear error messages
- Use React Hook Form for performance
- Use Formik for complex forms
- Handle form submission properly
- Improve user experience
Related Resources
Next Steps
- Learn about Performance Optimization
- Explore Vue.js
- Study Angular
- Practice form handling
- Build complex forms
Comments