表单是 Web 应用中最常见的交互元素之一,也是用户与应用进行数据交换的重要方式。在 React 中,表单处理有多种实现方式,从简单的受控组件到复杂的表单库。本文将详细介绍 React 表单处理的最佳实践,包括受控组件、表单验证、性能优化、第三方库使用等,并提供完整的实战案例。
1. 表单处理基础
1.1 受控组件 vs 非受控组件
受控组件
受控组件是 React 中处理表单的推荐方式,表单元素的值由 React 状态控制:
import { useState } from 'react';
function ControlledInput() {
const [value, setValue] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Submitted value:', value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={handleChange}
placeholder="Enter something"
/>
<button type="submit">Submit</button>
</form>
);
}
非受控组件
非受控组件使用 ref 直接访问 DOM 元素的值:
import { useRef } from 'react';
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (inputRef.current) {
console.log('Submitted value:', inputRef.current.value);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
ref={inputRef}
placeholder="Enter something"
defaultValue="Initial value"
/>
<button type="submit">Submit</button>
</form>
);
}
1.2 选择建议
| 组件类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 受控组件 | 需要实时验证、格式化、或与其他表单字段联动 | 状态可控,易于验证,支持实时反馈 | 可能导致频繁重渲染 |
| 非受控组件 | 简单表单,文件上传,或需要直接访问 DOM | 实现简单,性能更好 | 状态不可控,难以实现复杂验证 |
2. 表单状态管理
2.1 单个表单字段
对于单个表单字段,使用 useState 管理状态:
import { useState } from 'react';
function SingleFieldForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
// 简单验证
if (!value.includes('@')) {
setError('Please enter a valid email');
} else {
setError('');
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!error && email) {
console.log('Submitted email:', email);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={email}
onChange={handleEmailChange}
placeholder="Enter your email"
/>
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
<button type="submit" disabled={!!error || !email}>
Submit
</button>
</form>
);
}
2.2 多个表单字段
对于多个表单字段,可以使用单个 useState 对象管理所有字段:
import { useState } from 'react';
interface FormData {
name: string;
email: string;
password: string;
}
function MultiFieldForm() {
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
password: ''
});
const [errors, setErrors] = useState<Partial<FormData>>({
name: '',
email: '',
password: ''
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// 简单验证
if (name === 'name' && !value) {
setErrors(prev => ({ ...prev, name: 'Name is required' }));
} else if (name === 'email' && !value.includes('@')) {
setErrors(prev => ({ ...prev, email: 'Please enter a valid email' }));
} else if (name === 'password' && value.length < 6) {
setErrors(prev => ({ ...prev, password: 'Password must be at least 6 characters' }));
} else {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 最终验证
const newErrors: Partial<FormData> = {};
if (!formData.name) newErrors.name = 'Name is required';
if (!formData.email.includes('@')) newErrors.email = 'Please enter a valid email';
if (formData.password.length < 6) newErrors.password = 'Password must be at least 6 characters';
if (Object.keys(newErrors).length === 0) {
console.log('Submitted form data:', formData);
} else {
setErrors(newErrors);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Enter your name"
/>
{errors.name && <p style={{ color: 'red' }}>{errors.name}</p>}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Enter your email"
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter your password"
/>
{errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
}
2.3 使用 useReducer 管理复杂表单
对于复杂表单,可以使用 useReducer 管理状态:
import { useReducer } from 'react';
interface FormState {
name: string;
email: string;
password: string;
errors: {
name: string;
email: string;
password: string;
};
isSubmitting: boolean;
}
type FormAction =
| { type: 'UPDATE_FIELD'; field: keyof Omit<FormState, 'errors' | 'isSubmitting'>; value: string }
| { type: 'SET_ERRORS'; errors: Partial<FormState['errors']> }
| { type: 'SET_SUBMITTING'; isSubmitting: boolean }
| { type: 'RESET' };
const initialState: FormState = {
name: '',
email: '',
password: '',
errors: {
name: '',
email: '',
password: '',
},
isSubmitting: false,
};
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
[action.field]: action.value,
errors: {
...state.errors,
[action.field]: '',
},
};
case 'SET_ERRORS':
return {
...state,
errors: {
...state.errors,
...action.errors,
},
};
case 'SET_SUBMITTING':
return {
...state,
isSubmitting: action.isSubmitting,
};
case 'RESET':
return initialState;
default:
return state;
}
}
function ComplexForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
dispatch({ type: 'UPDATE_FIELD', field: name as keyof Omit<FormState, 'errors' | 'isSubmitting'>, value });
};
const validateForm = (): boolean => {
const errors: Partial<FormState['errors']> = {};
if (!state.name) errors.name = 'Name is required';
if (!state.email.includes('@')) errors.email = 'Please enter a valid email';
if (state.password.length < 6) errors.password = 'Password must be at least 6 characters';
if (Object.keys(errors).length > 0) {
dispatch({ type: 'SET_ERRORS', errors });
return false;
}
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
dispatch({ type: 'SET_SUBMITTING', isSubmitting: true });
// 模拟 API 调用
try {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Submitted form data:', state);
dispatch({ type: 'RESET' });
} catch (error) {
console.error('Submission error:', error);
} finally {
dispatch({ type: 'SET_SUBMITTING', isSubmitting: false });
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={state.name}
onChange={handleChange}
placeholder="Enter your name"
/>
{state.errors.name && <p style={{ color: 'red' }}>{state.errors.name}</p>}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={state.email}
onChange={handleChange}
placeholder="Enter your email"
/>
{state.errors.email && <p style={{ color: 'red' }}>{state.errors.email}</p>}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={state.password}
onChange={handleChange}
placeholder="Enter your password"
/>
{state.errors.password && <p style={{ color: 'red' }}>{state.errors.password}</p>}
</div>
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
3. 表单验证
3.1 客户端验证
内置表单验证
HTML5 提供了内置的表单验证:
function BuiltInValidationForm() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const form = e.currentTarget;
if (form.checkValidity()) {
console.log('Form is valid');
} else {
form.reportValidity();
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
required
minLength={2}
placeholder="Enter your name"
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
required
placeholder="Enter your email"
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
required
minLength={6}
placeholder="Enter your password"
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
自定义验证
使用 JavaScript 实现自定义验证逻辑:
import { useState } from 'react';
interface FormData {
name: string;
email: string;
password: string;
confirmPassword: string;
}
function CustomValidationForm() {
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState<Partial<FormData>>({});
const validateField = (name: keyof FormData, value: string) => {
let error = '';
switch (name) {
case 'name':
if (!value) {
error = 'Name is required';
} else if (value.length < 2) {
error = 'Name must be at least 2 characters';
}
break;
case 'email':
if (!value) {
error = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
error = 'Please enter a valid email';
}
break;
case 'password':
if (!value) {
error = 'Password is required';
} else if (value.length < 6) {
error = 'Password must be at least 6 characters';
} else if (!/[A-Z]/.test(value)) {
error = 'Password must contain at least one uppercase letter';
} else if (!/[0-9]/.test(value)) {
error = 'Password must contain at least one number';
}
break;
case 'confirmPassword':
if (!value) {
error = 'Please confirm your password';
} else if (value !== formData.password) {
error = 'Passwords do not match';
}
break;
default:
break;
}
return error;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
const error = validateField(name as keyof FormData, value);
setErrors(prev => ({
...prev,
[name]: error
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// 验证所有字段
const newErrors: Partial<FormData> = {};
Object.entries(formData).forEach(([name, value]) => {
const error = validateField(name as keyof FormData, value);
if (error) {
newErrors[name as keyof FormData] = error;
}
});
if (Object.keys(newErrors).length === 0) {
console.log('Submitted form data:', formData);
} else {
setErrors(newErrors);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Enter your name"
/>
{errors.name && <p style={{ color: 'red' }}>{errors.name}</p>}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Enter your email"
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter your password"
/>
{errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
placeholder="Confirm your password"
/>
{errors.confirmPassword && <p style={{ color: 'red' }}>{errors.confirmPassword}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
}
3.2 服务端验证
服务端验证是确保数据安全性的重要环节:
import { useState } from 'react';
interface FormData {
email: string;
password: string;
}
function ServerValidationForm() {
const [formData, setFormData] = useState<FormData>({
email: '',
password: ''
});
const [errors, setErrors] = useState<Partial<FormData>>({});
const [serverError, setServerError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
setErrors(prev => ({
...prev,
[name]: ''
}));
setServerError('');
};
const validateForm = (): boolean => {
const newErrors: Partial<FormData> = {};
if (!formData.email) newErrors.email = 'Email is required';
if (!formData.email.includes('@')) newErrors.email = 'Please enter a valid email';
if (!formData.password) newErrors.password = 'Password is required';
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return false;
}
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
setServerError('');
try {
// 模拟 API 调用
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
const errorData = await response.json();
if (errorData.errors) {
setErrors(errorData.errors);
} else {
setServerError('Invalid email or password');
}
return;
}
const data = await response.json();
console.log('Login successful:', data);
} catch (error) {
setServerError('An error occurred. Please try again.');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
{serverError && <p style={{ color: 'red' }}>{serverError}</p>}
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Enter your email"
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter your password"
/>
{errors.password && <p style={{ color: 'red' }}>{errors.password}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
4. 表单性能优化
4.1 防抖与节流
使用防抖减少频繁的状态更新:
import { useState, useCallback } from 'react';
function DebouncedInput() {
const [value, setValue] = useState('');
const [debouncedValue, setDebouncedValue] = useState('');
// 防抖函数
const useDebounce = (callback: Function, delay: number) => {
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
return useCallback((...args: any[]) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
const id = setTimeout(() => {
callback(...args);
}, delay);
setTimeoutId(id);
}, [callback, delay, timeoutId]);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
debouncedHandleChange(e.target.value);
};
const debouncedHandleChange = useDebounce((newValue: string) => {
setDebouncedValue(newValue);
// 这里可以进行 API 调用等昂贵操作
console.log('Debounced value:', newValue);
}, 500);
return (
<div>
<input
type="text"
value={value}
onChange={handleChange}
placeholder="Type something..."
/>
<p>Current value: {value}</p>
<p>Debounced value: {debouncedValue}</p>
</div>
);
}
4.2 批量更新
使用 useReducer 或批量更新函数减少重渲染:
import { useReducer } from 'react';
interface FormState {
firstName: string;
lastName: string;
email: string;
phone: string;
address: string;
city: string;
state: string;
zip: string;
}
type FormAction =
| { type: 'UPDATE_FIELD'; field: keyof FormState; value: string }
| { type: 'RESET' };
const initialState: FormState = {
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
city: '',
state: '',
zip: '',
};
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'UPDATE_FIELD':
return {
...state,
[action.field]: action.value,
};
case 'RESET':
return initialState;
default:
return state;
}
}
function PerformanceOptimizedForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
dispatch({ type: 'UPDATE_FIELD', field: name as keyof FormState, value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Submitted form data:', state);
};
return (
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="firstName">First Name:</label>
<input
type="text"
id="firstName"
name="firstName"
value={state.firstName}
onChange={handleChange}
placeholder="Enter your first name"
/>
</div>
<div>
<label htmlFor="lastName">Last Name:</label>
<input
type="text"
id="lastName"
name="lastName"
value={state.lastName}
onChange={handleChange}
placeholder="Enter your last name"
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={state.email}
onChange={handleChange}
placeholder="Enter your email"
/>
</div>
<div>
<label htmlFor="phone">Phone:</label>
<input
type="tel"
id="phone"
name="phone"
value={state.phone}
onChange={handleChange}
placeholder="Enter your phone number"
/>
</div>
<div className="md:col-span-2">
<label htmlFor="address">Address:</label>
<input
type="text"
id="address"
name="address"
value={state.address}
onChange={handleChange}
placeholder="Enter your address"
/>
</div>
<div>
<label htmlFor="city">City:</label>
<input
type="text"
id="city"
name="city"
value={state.city}
onChange={handleChange}
placeholder="Enter your city"
/>
</div>
<div>
<label htmlFor="state">State:</label>
<input
type="text"
id="state"
name="state"
value={state.state}
onChange={handleChange}
placeholder="Enter your state"
/>
</div>
<div>
<label htmlFor="zip">Zip Code:</label>
<input
type="text"
id="zip"
name="zip"
value={state.zip}
onChange={handleChange}
placeholder="Enter your zip code"
/>
</div>
</div>
<button type="submit">Submit</button>
</form>
);
}
4.3 虚拟化长表单
对于长表单,可以使用虚拟化技术只渲染可视区域的表单字段:
import { useState, useRef, useMemo } from 'react';
interface FormField {
id: string;
name: string;
label: string;
type: string;
placeholder: string;
}
const formFields: FormField[] = [
{ id: '1', name: 'field1', label: 'Field 1', type: 'text', placeholder: 'Enter field 1' },
{ id: '2', name: 'field2', label: 'Field 2', type: 'text', placeholder: 'Enter field 2' },
{ id: '3', name: 'field3', label: 'Field 3', type: 'text', placeholder: 'Enter field 3' },
// ... 更多字段
{ id: '50', name: 'field50', label: 'Field 50', type: 'text', placeholder: 'Enter field 50' },
];
function VirtualizedForm() {
const [formData, setFormData] = useState<Record<string, string>>({});
const containerRef = useRef<HTMLDivElement>(null);
const [visibleFields, setVisibleFields] = useState<FormField[]>([]);
// 简单的虚拟化实现
useMemo(() => {
// 这里可以根据容器的滚动位置和高度计算可见字段
// 简化实现:只显示前 10 个字段
setVisibleFields(formFields.slice(0, 10));
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Submitted form data:', formData);
};
return (
<div ref={containerRef} style={{ maxHeight: '400px', overflow: 'auto' }}>
<form onSubmit={handleSubmit}>
{visibleFields.map(field => (
<div key={field.id} style={{ marginBottom: '16px' }}>
<label htmlFor={field.name}>{field.label}:</label>
<input
type={field.type}
id={field.name}
name={field.name}
value={formData[field.name] || ''}
onChange={handleChange}
placeholder={field.placeholder}
/>
</div>
))}
<button type="submit">Submit</button>
</form>
</div>
);
}
5. 第三方表单库
5.1 Formik
Formik 是一个流行的表单库,简化了表单状态管理和验证:
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
interface FormValues {
name: string;
email: string;
password: string;
}
const validationSchema = Yup.object({
name: Yup.string()
.required('Name is required')
.min(2, 'Name must be at least 2 characters'),
email: Yup.string()
.required('Email is required')
.email('Please enter a valid email'),
password: Yup.string()
.required('Password is required')
.min(6, 'Password must be at least 6 characters')
.matches(/[A-Z]/, 'Password must contain at least one uppercase letter')
.matches(/[0-9]/, 'Password must contain at least one number'),
});
function FormikForm() {
const initialValues: FormValues = {
name: '',
email: '',
password: '',
};
const handleSubmit = (values: FormValues) => {
console.log('Submitted form data:', values);
};
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<div>
<label htmlFor="name">Name:</label>
<Field
type="text"
id="name"
name="name"
placeholder="Enter your name"
/>
<ErrorMessage name="name" component="p" style={{ color: 'red' }} />
</div>
<div>
<label htmlFor="email">Email:</label>
<Field
type="email"
id="email"
name="email"
placeholder="Enter your email"
/>
<ErrorMessage name="email" component="p" style={{ color: 'red' }} />
</div>
<div>
<label htmlFor="password">Password:</label>
<Field
type="password"
id="password"
name="password"
placeholder="Enter your password"
/>
<ErrorMessage name="password" component="p" style={{ color: 'red' }} />
</div>
<button type="submit">Submit</button>
</Form>
</Formik>
);
}
5.2 React Hook Form
React Hook Form 是一个轻量级的表单库,注重性能:
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';
interface FormValues {
name: string;
email: string;
password: string;
}
const validationSchema = Yup.object({
name: Yup.string()
.required('Name is required')
.min(2, 'Name must be at least 2 characters'),
email: Yup.string()
.required('Email is required')
.email('Please enter a valid email'),
password: Yup.string()
.required('Password is required')
.min(6, 'Password must be at least 6 characters'),
});
function ReactHookForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: yupResolver(validationSchema),
defaultValues: {
name: '',
email: '',
password: '',
},
});
const onSubmit = (data: FormValues) => {
console.log('Submitted form data:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
{...register('name')}
placeholder="Enter your name"
/>
{errors.name && <p style={{ color: 'red' }}>{errors.name.message}</p>}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
{...register('email')}
placeholder="Enter your email"
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
{...register('password')}
placeholder="Enter your password"
/>
{errors.password && <p style={{ color: 'red' }}>{errors.password.message}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
}
5.3 Final Form
Final Form 是一个框架无关的表单库,提供了强大的表单状态管理:
import { Form, Field } from 'react-final-form';
interface FormValues {
name: string;
email: string;
password: string;
}
function FinalFormForm() {
const validate = (values: FormValues) => {
const errors: Partial<FormValues> = {};
if (!values.name) {
errors.name = 'Name is required';
} else if (values.name.length < 2) {
errors.name = 'Name must be at least 2 characters';
}
if (!values.email) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = 'Please enter a valid email';
}
if (!values.password) {
errors.password = 'Password is required';
} else if (values.password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
return errors;
};
const onSubmit = (values: FormValues) => {
console.log('Submitted form data:', values);
};
return (
<Form
onSubmit={onSubmit}
validate={validate}
render={({ handleSubmit, submitting, pristine }) => (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<Field name="name">
{({ input, meta }) => (
<>
<input
{...input}
type="text"
id="name"
placeholder="Enter your name"
/>
{meta.error && meta.touched && <p style={{ color: 'red' }}>{meta.error}</p>}
</>
)}
</Field>
</div>
<div>
<label htmlFor="email">Email:</label>
<Field name="email">
{({ input, meta }) => (
<>
<input
{...input}
type="email"
id="email"
placeholder="Enter your email"
/>
{meta.error && meta.touched && <p style={{ color: 'red' }}>{meta.error}</p>}
</>
)}
</Field>
</div>
<div>
<label htmlFor="password">Password:</label>
<Field name="password">
{({ input, meta }) => (
<>
<input
{...input}
type="password"
id="password"
placeholder="Enter your password"
/>
{meta.error && meta.touched && <p style={{ color: 'red' }}>{meta.error}</p>}
</>
)}
</Field>
</div>
<button type="submit" disabled={submitting || pristine}>
{submitting ? 'Submitting...' : 'Submit'}
</button>
</form>
)}
/>
);
}
6. 实战案例
6.1 登录表单
功能需求:
- 邮箱和密码输入
- 客户端验证
- 服务端验证
- 加载状态
- 错误处理
实现:
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
interface LoginFormData {
email: string;
password: string;
}
function LoginForm() {
const [formData, setFormData] = useState<LoginFormData>({
email: '',
password: ''
});
const [errors, setErrors] = useState<Partial<LoginFormData>>({});
const [serverError, setServerError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const navigate = useNavigate();
const validateForm = (): boolean => {
const newErrors: Partial<LoginFormData> = {};
if (!formData.email) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Please enter a valid email';
}
if (!formData.password) {
newErrors.password = 'Password is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
setErrors(prev => ({
...prev,
[name]: ''
}));
setServerError('');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
setServerError('');
try {
// 模拟 API 调用
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
const errorData = await response.json();
if (errorData.errors) {
setErrors(errorData.errors);
} else {
setServerError('Invalid email or password');
}
return;
}
const data = await response.json();
console.log('Login successful:', data);
navigate('/dashboard');
} catch (error) {
setServerError('An error occurred. Please try again.');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="login-form">
<h1>Login</h1>
{serverError && <p className="server-error">{serverError}</p>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Enter your email"
disabled={isSubmitting}
/>
{errors.email && <p className="error-message">{errors.email}</p>}
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter your password"
disabled={isSubmitting}
/>
{errors.password && <p className="error-message">{errors.password}</p>}
</div>
<div className="form-actions">
<button
type="submit"
className="login-button"
disabled={isSubmitting}
>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
<a href="/forgot-password" className="forgot-password">
Forgot password?
</a>
</div>
</form>
<div className="register-link">
Don't have an account? <a href="/register">Register</a>
</div>
</div>
);
}
export default LoginForm;
6.2 注册表单
功能需求:
- 多字段输入(姓名、邮箱、密码、确认密码)
- 复杂验证规则
- 密码强度指示器
- 提交反馈
实现:
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
interface RegisterFormData {
firstName: string;
lastName: string;
email: string;
password: string;
confirmPassword: string;
}
function RegisterForm() {
const [formData, setFormData] = useState<RegisterFormData>({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState<Partial<RegisterFormData>>({});
const [serverError, setServerError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const navigate = useNavigate();
const getPasswordStrength = (password: string): 'weak' | 'medium' | 'strong' => {
if (password.length < 6) return 'weak';
if (password.length < 8) return 'medium';
if (/[A-Z]/.test(password) && /[0-9]/.test(password)) return 'strong';
return 'medium';
};
const validateField = (name: keyof RegisterFormData, value: string): string => {
switch (name) {
case 'firstName':
if (!value) return 'First name is required';
if (value.length < 2) return 'First name must be at least 2 characters';
return '';
case 'lastName':
if (!value) return 'Last name is required';
if (value.length < 2) return 'Last name must be at least 2 characters';
return '';
case 'email':
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Please enter a valid email';
return '';
case 'password':
if (!value) return 'Password is required';
if (value.length < 6) return 'Password must be at least 6 characters';
if (!/[A-Z]/.test(value)) return 'Password must contain at least one uppercase letter';
if (!/[0-9]/.test(value)) return 'Password must contain at least one number';
return '';
case 'confirmPassword':
if (!value) return 'Please confirm your password';
if (value !== formData.password) return 'Passwords do not match';
return '';
default:
return '';
}
};
const validateForm = (): boolean => {
const newErrors: Partial<RegisterFormData> = {};
Object.entries(formData).forEach(([name, value]) => {
const error = validateField(name as keyof RegisterFormData, value);
if (error) {
newErrors[name as keyof RegisterFormData] = error;
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
const error = validateField(name as keyof RegisterFormData, value);
setErrors(prev => ({
...prev,
[name]: error
}));
setServerError('');
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
setServerError('');
try {
// 模拟 API 调用
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
const errorData = await response.json();
if (errorData.errors) {
setErrors(errorData.errors);
} else {
setServerError('Registration failed. Please try again.');
}
return;
}
const data = await response.json();
console.log('Registration successful:', data);
navigate('/login');
} catch (error) {
setServerError('An error occurred. Please try again.');
} finally {
setIsSubmitting(false);
}
};
const passwordStrength = getPasswordStrength(formData.password);
return (
<div className="register-form">
<h1>Register</h1>
{serverError && <p className="server-error">{serverError}</p>}
<form onSubmit={handleSubmit}>
<div className="form-row">
<div className="form-group">
<label htmlFor="firstName">First Name:</label>
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="Enter your first name"
disabled={isSubmitting}
/>
{errors.firstName && <p className="error-message">{errors.firstName}</p>}
</div>
<div className="form-group">
<label htmlFor="lastName">Last Name:</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Enter your last name"
disabled={isSubmitting}
/>
{errors.lastName && <p className="error-message">{errors.lastName}</p>}
</div>
</div>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Enter your email"
disabled={isSubmitting}
/>
{errors.email && <p className="error-message">{errors.email}</p>}
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter your password"
disabled={isSubmitting}
/>
{errors.password && <p className="error-message">{errors.password}</p>}
{formData.password && (
<div className="password-strength">
<div className={`strength-indicator ${passwordStrength}`}>
{passwordStrength === 'weak' && 'Weak'}
{passwordStrength === 'medium' && 'Medium'}
{passwordStrength === 'strong' && 'Strong'}
</div>
</div>
)}
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
placeholder="Confirm your password"
disabled={isSubmitting}
/>
{errors.confirmPassword && <p className="error-message">{errors.confirmPassword}</p>}
</div>
<div className="form-actions">
<button
type="submit"
className="register-button"
disabled={isSubmitting}
>
{isSubmitting ? 'Registering...' : 'Register'}
</button>
</div>
</form>
<div className="login-link">
Already have an account? <a href="/login">Login</a>
</div>
</div>
);
}
export default RegisterForm;
6.3 联系表单
功能需求:
- 姓名、邮箱、主题、消息输入
- 实时验证
- 提交状态反馈
- 成功/失败提示
实现:
import { useState } from 'react';
interface ContactFormData {
name: string;
email: string;
subject: string;
message: string;
}
function ContactForm() {
const [formData, setFormData] = useState<ContactFormData>({
name: '',
email: '',
subject: '',
message: ''
});
const [errors, setErrors] = useState<Partial<ContactFormData>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
const validateField = (name: keyof ContactFormData, value: string): string => {
switch (name) {
case 'name':
if (!value) return 'Name is required';
if (value.length < 2) return 'Name must be at least 2 characters';
return '';
case 'email':
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Please enter a valid email';
return '';
case 'subject':
if (!value) return 'Subject is required';
return '';
case 'message':
if (!value) return 'Message is required';
if (value.length < 10) return 'Message must be at least 10 characters';
return '';
default:
return '';
}
};
const validateForm = (): boolean => {
const newErrors: Partial<ContactFormData> = {};
Object.entries(formData).forEach(([name, value]) => {
const error = validateField(name as keyof ContactFormData, value);
if (error) {
newErrors[name as keyof ContactFormData] = error;
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
const error = validateField(name as keyof ContactFormData, value);
setErrors(prev => ({
...prev,
[name]: error
}));
setIsSuccess(false);
setIsError(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
setIsSuccess(false);
setIsError(false);
try {
// 模拟 API 调用
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error('Submission failed');
}
setIsSuccess(true);
// 重置表单
setFormData({
name: '',
email: '',
subject: '',
message: ''
});
} catch (error) {
setIsError(true);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="contact-form">
<h1>Contact Us</h1>
{isSuccess && (
<div className="success-message">
Thank you for your message! We'll get back to you soon.
</div>
)}
{isError && (
<div className="error-message">
Oops! Something went wrong. Please try again later.
</div>
)}
<form onSubmit={handleSubmit}>
<div className="form-row">
<div className="form-group">
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Enter your name"
disabled={isSubmitting}
/>
{errors.name && <p className="field-error">{errors.name}</p>}
</div>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Enter your email"
disabled={isSubmitting}
/>
{errors.email && <p className="field-error">{errors.email}</p>}
</div>
</div>
<div className="form-group">
<label htmlFor="subject">Subject:</label>
<input
type="text"
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
placeholder="Enter subject"
disabled={isSubmitting}
/>
{errors.subject && <p className="field-error">{errors.subject}</p>}
</div>
<div className="form-group">
<label htmlFor="message">Message:</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Enter your message"
rows={5}
disabled={isSubmitting}
></textarea>
{errors.message && <p className="field-error">{errors.message}</p>}
</div>
<div className="form-actions">
<button
type="submit"
className="submit-button"
disabled={isSubmitting}
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</div>
</form>
</div>
);
}
export default ContactForm;
7. 最佳实践总结
7.1 表单设计
- 清晰的表单结构:使用分组和标签组织表单字段
- 合理的验证规则:根据字段类型设置适当的验证规则
- 实时反馈:提供即时的验证反馈和错误提示
- 用户友好的错误信息:错误信息应该清晰、具体、有帮助
- 响应式设计:确保表单在不同设备上都能正常显示和使用
7.2 状态管理
- 使用受控组件:对于需要实时验证或格式化的表单,使用受控组件
- 合理的状态结构:根据表单复杂度选择合适的状态管理方案
- 批量更新:使用
useReducer或批量更新函数减少重渲染 - 防抖处理:对于频繁更新的字段,使用防抖减少状态更新次数
7.3 验证策略
- 客户端验证:提供即时的验证反馈,提升用户体验
- 服务端验证:确保数据安全性和一致性
- 分层验证:在字段级别、表单级别和服务端分别进行验证
- 合理的验证时机:实时验证、提交验证和服务端验证相结合
7.4 性能优化
- 减少重渲染:使用
useCallback、useMemo和React.memo优化性能 - 虚拟化:对于长表单,使用虚拟化技术减少 DOM 节点数量
- 异步处理:将昂贵的操作(如 API 调用)移到异步函数中
- 代码分割:对于大型表单库,使用代码分割减少初始加载时间
7.5 安全性
- 输入验证:验证所有用户输入,防止恶意数据
- 密码安全:使用 HTTPS 传输敏感数据,避免在客户端存储密码
- CSRF 保护:实现 CSRF 令牌,防止跨站请求伪造
- 数据脱敏:在日志和错误信息中避免暴露敏感数据
7.6 可访问性
- 语义化 HTML:使用适当的 HTML 元素和属性
- 表单标签:为所有表单字段提供明确的标签
- 错误提示:确保错误信息与相关字段关联
- 键盘导航:确保表单可以通过键盘导航
- 屏幕阅读器支持:使用 ARIA 属性增强屏幕阅读器支持
总结
React 表单处理是一个复杂但重要的话题,本文介绍了从基础到高级的各种表单处理技术:
- 表单基础:受控组件 vs 非受控组件
- 状态管理:从
useState到useReducer - 表单验证:客户端验证和服务端验证
- 性能优化:防抖、节流和虚拟化
- 第三方库:Formik、React Hook Form 和 Final Form
- 实战案例:登录、注册和联系表单
- 最佳实践:表单设计、状态管理、验证策略等
通过掌握这些技术和最佳实践,你可以构建出用户体验良好、性能优异、安全可靠的 React 表单。无论你是构建简单的联系表单还是复杂的多步骤表单,这些技术都能帮助你实现高质量的表单解决方案。