作为 Vue 开发者,在迁移到 React 开发时,表单处理的差异是一个重要的适应点。本文将从 Vue 开发者熟悉的角度出发,详细介绍 React 中的表单处理方式和最佳实践。
基础表单处理对比
Vue 的表单处理
在 Vue 中,我们习惯使用 v-model
进行双向绑定:
vue
<template>
<form @submit.prevent="handleSubmit">
<div>
<label>用户名:</label>
<input v-model="form.username" type="text" />
</div>
<div>
<label>密码:</label>
<input v-model="form.password" type="password" />
</div>
<div>
<label>记住我:</label>
<input v-model="form.remember" type="checkbox" />
</div>
<button type="submit">登录</button>
</form>
</template>
<script>
export default {
data() {
return {
form: {
username: '',
password: '',
remember: false
}
}
},
methods: {
handleSubmit() {
console.log('表单数据:', this.form);
}
}
}
</script>
React 的表单处理
在 React 中,我们需要手动处理表单状态:
jsx
import React, { useState } from 'react';
function LoginForm() {
const [form, setForm] = useState({
username: '',
password: '',
remember: false
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('表单数据:', form);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>用户名:</label>
<input
name="username"
type="text"
value={form.username}
onChange={handleChange}
/>
</div>
<div>
<label>密码:</label>
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
/>
</div>
<div>
<label>记住我:</label>
<input
name="remember"
type="checkbox"
checked={form.remember}
onChange={handleChange}
/>
</div>
<button type="submit">登录</button>
</form>
);
}
主要区别:
- 数据绑定方式
- Vue 使用 v-model 实现双向绑定
- React 需要手动处理 value 和 onChange
- 事件处理方式
- Vue 可以直接修改数据
- React 需要通过 setState 更新状态
- 表单提交处理
- Vue 使用 @submit.prevent
- React 需要手动调用 e.preventDefault()
表单验证
1. 自定义 Hook 实现表单验证
jsx
// useForm.ts
import { useState, useCallback } from 'react';
interface ValidationRule {
required?: boolean;
pattern?: RegExp;
minLength?: number;
maxLength?: number;
validate?: (value: any) => boolean | string;
}
interface ValidationSchema {
[field: string]: ValidationRule;
}
function useForm<T extends object>(initialValues: T, validationSchema?: ValidationSchema) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const validateField = useCallback((name: keyof T, value: any) => {
if (!validationSchema?.[name]) return '';
const rules = validationSchema[name];
if (rules.required && !value) {
return '此字段是必填的';
}
if (rules.pattern && !rules.pattern.test(value)) {
return '格式不正确';
}
if (rules.minLength && value.length < rules.minLength) {
return `最少需要 ${rules.minLength} 个字符`;
}
if (rules.maxLength && value.length > rules.maxLength) {
return `最多允许 ${rules.maxLength} 个字符`;
}
if (rules.validate) {
const result = rules.validate(value);
if (typeof result === 'string') return result;
if (!result) return '验证失败';
}
return '';
}, [validationSchema]);
const handleChange = useCallback((name: keyof T, value: any) => {
setValues(prev => ({ ...prev, [name]: value }));
const error = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
}, [validateField]);
const handleBlur = useCallback((name: keyof T) => {
setTouched(prev => ({ ...prev, [name]: true }));
const error = validateField(name, values[name]);
setErrors(prev => ({ ...prev, [name]: error }));
}, [validateField, values]);
const validateForm = useCallback(() => {
const newErrors: Partial<Record<keyof T, string>> = {};
let isValid = true;
Object.keys(validationSchema || {}).forEach(field => {
const error = validateField(field as keyof T, values[field as keyof T]);
if (error) {
newErrors[field as keyof T] = error;
isValid = false;
}
});
setErrors(newErrors);
return isValid;
}, [validateField, values, validationSchema]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
validateForm,
setValues,
setErrors,
setTouched
};
}
export default useForm;
2. 表单组件封装
jsx
// FormField.tsx
interface FormFieldProps {
name: string;
label: string;
type?: string;
placeholder?: string;
required?: boolean;
}
function FormField({
name,
label,
type = 'text',
placeholder,
required
}: FormFieldProps) {
const { values, errors, touched, handleChange, handleBlur } = useFormContext();
const hasError = touched[name] && errors[name];
return (
<div className="form-field">
<label>
{label}
{required && <span className="required">*</span>}
</label>
<input
type={type}
name={name}
value={values[name] || ''}
onChange={e => handleChange(name, e.target.value)}
onBlur={() => handleBlur(name)}
placeholder={placeholder}
className={hasError ? 'error' : ''}
/>
{hasError && <div className="error-message">{errors[name]}</div>}
</div>
);
}
// Form.tsx
interface FormProps {
initialValues: object;
validationSchema?: object;
onSubmit: (values: object) => void;
children: React.ReactNode;
}
const FormContext = React.createContext({});
function Form({
initialValues,
validationSchema,
onSubmit,
children
}: FormProps) {
const form = useForm(initialValues, validationSchema);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (form.validateForm()) {
onSubmit(form.values);
}
};
return (
<FormContext.Provider value={form}>
<form onSubmit={handleSubmit}>
{children}
</form>
</FormContext.Provider>
);
}
实战示例:注册表单
让我们通过一个完整的注册表单来实践这些概念:
jsx
// RegisterForm.tsx
import React from 'react';
import Form from './components/Form';
import FormField from './components/FormField';
import useForm from './hooks/useForm';
const validationSchema = {
username: {
required: true,
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]+$/,
validate: (value) => {
// 模拟异步验证
return new Promise((resolve) => {
setTimeout(() => {
resolve(value !== 'admin' || '用户名已被占用');
}, 1000);
});
}
},
email: {
required: true,
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
},
password: {
required: true,
minLength: 8,
validate: (value) => {
if (!/[A-Z]/.test(value)) {
return '密码必须包含大写字母';
}
if (!/[a-z]/.test(value)) {
return '密码必须包含小写字母';
}
if (!/[0-9]/.test(value)) {
return '密码必须包含数字';
}
return true;
}
},
confirmPassword: {
required: true,
validate: (value, values) => {
if (value !== values.password) {
return '两次输入的密码不一致';
}
return true;
}
},
phone: {
pattern: /^1[3-9]\d{9}$/,
validate: async (value) => {
if (!value) return true;
// 模拟异步验证
const response = await fetch(`/api/validate-phone?phone=${value}`);
const { isValid } = await response.json();
return isValid || '手机号已被注册';
}
},
agreement: {
required: true,
validate: (value) => {
return value === true || '请同意用户协议';
}
}
};
function RegisterForm() {
const handleSubmit = async (values) => {
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values)
});
if (!response.ok) {
throw new Error('注册失败');
}
// 注册成功,跳转到登录页
navigate('/login');
} catch (error) {
console.error('注册错误:', error);
}
};
return (
<Form
initialValues={{
username: '',
email: '',
password: '',
confirmPassword: '',
phone: '',
agreement: false
}}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<FormField
name="username"
label="用户名"
required
placeholder="请输入用户名"
/>
<FormField
name="email"
label="邮箱"
type="email"
required
placeholder="请输入邮箱"
/>
<FormField
name="password"
label="密码"
type="password"
required
placeholder="请输入密码"
/>
<FormField
name="confirmPassword"
label="确认密码"
type="password"
required
placeholder="请再次输入密码"
/>
<FormField
name="phone"
label="手机号"
placeholder="请输入手机号(选填)"
/>
<FormField
name="agreement"
label="我已阅读并同意用户协议"
type="checkbox"
required
/>
<button type="submit">注册</button>
</Form>
);
}
性能优化
-
表单字段独立渲染
jsxconst FormField = React.memo(function FormField({ name, ...props }) { return <Field name={name} {...props} />; });
-
延迟验证
jsxconst debouncedValidate = useCallback( debounce((name, value) => { validateField(name, value); }, 500), [] );
-
按需更新
jsxconst handleChange = (name, value) => { setValues(prev => ({ ...prev, [name]: value })); // 只在必要时触发验证 if (touched[name] || errors[name]) { debouncedValidate(name, value); } };
最佳实践
-
表单设计原则
- 即时反馈
- 清晰的错误提示
- 合理的默认值
- 友好的用户体验
-
验证策略
- 客户端预校验
- 服务端最终校验
- 适时的异步验证
- 合理的错误处理
-
性能考虑
- 避免不必要的渲染
- 延迟验证
- 缓存验证结果
- 优化大表单性能
小结
-
React 表单处理的特点:
- 受控组件模式
- 单向数据流
- 灵活的验证方案
- 组件化的表单设计
-
从 Vue 到 React 的转变:
- 告别 v-model
- 拥抱受控组件
- 自定义表单验证
- 性能优化思路
-
开发建议:
- 合理封装
- 注重复用
- 关注性能
- 优化体验
下一篇文章,我们将深入探讨 React 的性能优化策略,帮助你构建高性能的应用。
如果觉得这篇文章对你有帮助,别忘了点个赞 👍