Appearance
表单验证规则
概述
表单验证是确保数据质量和用户体验的关键环节。React Hook Form提供了强大而灵活的验证机制,支持同步验证、异步验证和复杂的自定义规则。本文将全面介绍表单验证的各种规则和最佳实践。
内置验证规则
required必填验证
jsx
import { useForm } from 'react-hook-form';
function RequiredValidation() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{/* 简单required */}
<input
{...register('username', {
required: true,
})}
/>
{errors.username && <span>此字段必填</span>}
{/* required with自定义消息 */}
<input
{...register('email', {
required: '邮箱不能为空',
})}
/>
{errors.email && <span>{errors.email.message}</span>}
{/* required with对象配置 */}
<input
{...register('password', {
required: {
value: true,
message: '密码不能为空',
},
})}
/>
{errors.password && <span>{errors.password.message}</span>}
{/* 条件required */}
<input
{...register('phone', {
required: (formValues) =>
formValues.contactMethod === 'phone' ? '电话号码不能为空' : false,
})}
/>
<button type="submit">提交</button>
</form>
);
}长度验证
jsx
function LengthValidation() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{/* minLength - 最小长度 */}
<input
{...register('username', {
required: '用户名不能为空',
minLength: {
value: 3,
message: '用户名至少3个字符',
},
})}
/>
{errors.username && <span>{errors.username.message}</span>}
{/* maxLength - 最大长度 */}
<input
{...register('bio', {
maxLength: {
value: 200,
message: '简介最多200个字符',
},
})}
/>
{errors.bio && <span>{errors.bio.message}</span>}
{/* 组合使用 */}
<input
{...register('password', {
required: '密码不能为空',
minLength: {
value: 8,
message: '密码至少8个字符',
},
maxLength: {
value: 20,
message: '密码最多20个字符',
},
})}
/>
{errors.password && <span>{errors.password.message}</span>}
{/* 实时显示字符数 */}
<textarea
{...register('description', {
maxLength: {
value: 500,
message: '描述最多500个字符',
},
})}
/>
<CharacterCounter name="description" maxLength={500} />
{errors.description && <span>{errors.description.message}</span>}
<button type="submit">提交</button>
</form>
);
}
// 字符计数器组件
function CharacterCounter({ name, maxLength }) {
const { watch } = useForm();
const value = watch(name) || '';
return (
<div className="character-counter">
{value.length} / {maxLength}
</div>
);
}数值范围验证
jsx
function NumberValidation() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{/* min - 最小值 */}
<input
type="number"
{...register('age', {
required: '年龄不能为空',
min: {
value: 18,
message: '必须年满18岁',
},
valueAsNumber: true,
})}
/>
{errors.age && <span>{errors.age.message}</span>}
{/* max - 最大值 */}
<input
type="number"
{...register('quantity', {
required: '数量不能为空',
max: {
value: 100,
message: '最多购买100件',
},
valueAsNumber: true,
})}
/>
{errors.quantity && <span>{errors.quantity.message}</span>}
{/* 组合min和max */}
<input
type="number"
{...register('rating', {
required: '评分不能为空',
min: {
value: 1,
message: '评分最低1分',
},
max: {
value: 5,
message: '评分最高5分',
},
valueAsNumber: true,
})}
/>
{errors.rating && <span>{errors.rating.message}</span>}
{/* 小数验证 */}
<input
type="number"
step="0.01"
{...register('price', {
required: '价格不能为空',
min: {
value: 0.01,
message: '价格必须大于0',
},
valueAsNumber: true,
})}
/>
{errors.price && <span>{errors.price.message}</span>}
<button type="submit">提交</button>
</form>
);
}正则表达式验证
jsx
function PatternValidation() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{/* 邮箱验证 */}
<input
{...register('email', {
required: '邮箱不能为空',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: '邮箱格式不正确',
},
})}
placeholder="邮箱"
/>
{errors.email && <span>{errors.email.message}</span>}
{/* 手机号验证 */}
<input
{...register('phone', {
required: '手机号不能为空',
pattern: {
value: /^1[3-9]\d{9}$/,
message: '手机号格式不正确',
},
})}
placeholder="手机号"
/>
{errors.phone && <span>{errors.phone.message}</span>}
{/* URL验证 */}
<input
{...register('website', {
pattern: {
value: /^https?:\/\/.+\..+/,
message: 'URL格式不正确',
},
})}
placeholder="网站"
/>
{errors.website && <span>{errors.website.message}</span>}
{/* 用户名验证(字母数字下划线) */}
<input
{...register('username', {
required: '用户名不能为空',
pattern: {
value: /^[a-zA-Z0-9_]+$/,
message: '只能包含字母、数字和下划线',
},
})}
placeholder="用户名"
/>
{errors.username && <span>{errors.username.message}</span>}
{/* 邮编验证 */}
<input
{...register('zipCode', {
required: '邮编不能为空',
pattern: {
value: /^\d{6}$/,
message: '邮编必须是6位数字',
},
})}
placeholder="邮编"
/>
{errors.zipCode && <span>{errors.zipCode.message}</span>}
{/* 身份证验证 */}
<input
{...register('idCard', {
required: '身份证号不能为空',
pattern: {
value: /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/,
message: '身份证号格式不正确',
},
})}
placeholder="身份证号"
/>
{errors.idCard && <span>{errors.idCard.message}</span>}
{/* 密码强度验证 */}
<input
type="password"
{...register('password', {
required: '密码不能为空',
pattern: {
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
message: '密码必须包含大小写字母、数字和特殊字符,至少8位',
},
})}
placeholder="密码"
/>
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">提交</button>
</form>
);
}自定义验证
单个自定义验证
jsx
function CustomValidation() {
const { register, handleSubmit, formState: { errors } } = useForm();
const validateUsername = (value) => {
// 返回true表示验证通过
if (value === 'admin') {
return '不能使用admin作为用户名';
}
if (value.includes(' ')) {
return '用户名不能包含空格';
}
if (/^\d/.test(value)) {
return '用户名不能以数字开头';
}
return true;
};
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input
{...register('username', {
required: '用户名不能为空',
validate: validateUsername,
})}
/>
{errors.username && <span>{errors.username.message}</span>}
<button type="submit">提交</button>
</form>
);
}多个自定义验证
jsx
function MultipleCustomValidation() {
const { register, handleSubmit, formState: { errors } } = useForm({
criteriaMode: 'all', // 显示所有错误
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input
{...register('username', {
required: '用户名不能为空',
validate: {
notAdmin: (value) =>
value !== 'admin' || '不能使用admin作为用户名',
noSpaces: (value) =>
!value.includes(' ') || '用户名不能包含空格',
notStartWithNumber: (value) =>
!/^\d/.test(value) || '用户名不能以数字开头',
notReserved: (value) => {
const reserved = ['root', 'system', 'administrator'];
return !reserved.includes(value.toLowerCase()) || '该用户名已被保留';
},
},
})}
/>
{/* 显示所有错误 */}
{errors.username && (
<div className="error-messages">
{errors.username.types ? (
Object.values(errors.username.types).map((error, index) => (
<div key={index}>{error}</div>
))
) : (
<div>{errors.username.message}</div>
)}
</div>
)}
<button type="submit">提交</button>
</form>
);
}访问其他字段值
jsx
function CrossFieldValidation() {
const { register, handleSubmit, watch, formState: { errors } } = useForm();
const password = watch('password');
const startDate = watch('startDate');
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{/* 密码确认 */}
<input
type="password"
{...register('password', {
required: '密码不能为空',
})}
placeholder="密码"
/>
<input
type="password"
{...register('confirmPassword', {
required: '请确认密码',
validate: (value) =>
value === password || '密码不匹配',
})}
placeholder="确认密码"
/>
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
{/* 日期范围验证 */}
<input
type="date"
{...register('startDate', {
required: '开始日期不能为空',
})}
/>
<input
type="date"
{...register('endDate', {
required: '结束日期不能为空',
validate: (value) => {
if (!startDate) return true;
return new Date(value) >= new Date(startDate) || '结束日期不能早于开始日期';
},
})}
/>
{errors.endDate && <span>{errors.endDate.message}</span>}
{/* 访问所有表单值 */}
<input
{...register('discountCode', {
validate: (value, formValues) => {
if (formValues.totalAmount < 100 && value) {
return '订单金额小于100元不能使用优惠码';
}
return true;
},
})}
placeholder="优惠码"
/>
<button type="submit">提交</button>
</form>
);
}异步验证
基础异步验证
jsx
function AsyncValidation() {
const { register, handleSubmit, formState: { errors, isValidating } } = useForm({
mode: 'onBlur',
});
const checkUsernameAvailability = async (username) => {
if (username.length < 3) {
return '用户名至少3个字符';
}
try {
const response = await fetch(`/api/check-username?username=${username}`);
const data = await response.json();
if (!data.available) {
return '该用户名已被使用';
}
return true;
} catch (error) {
return '验证失败,请重试';
}
};
const checkEmailAvailability = async (email) => {
try {
const response = await fetch(`/api/check-email?email=${email}`);
const data = await response.json();
return data.available || '该邮箱已被注册';
} catch (error) {
return '验证失败,请重试';
}
};
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<div>
<input
{...register('username', {
required: '用户名不能为空',
validate: checkUsernameAvailability,
})}
/>
{isValidating && <span>验证中...</span>}
{errors.username && <span>{errors.username.message}</span>}
</div>
<div>
<input
type="email"
{...register('email', {
required: '邮箱不能为空',
validate: checkEmailAvailability,
})}
/>
{isValidating && <span>验证中...</span>}
{errors.email && <span>{errors.email.message}</span>}
</div>
<button type="submit">提交</button>
</form>
);
}防抖异步验证
jsx
import { useCallback } from 'react';
import { debounce } from 'lodash';
function DebouncedAsyncValidation() {
const { register, handleSubmit, formState: { errors } } = useForm({
mode: 'onChange',
});
// 使用useCallback确保debounce函数稳定
const debouncedCheckUsername = useCallback(
debounce(async (username) => {
const response = await fetch(`/api/check-username?username=${username}`);
const data = await response.json();
return data.available;
}, 500),
[]
);
const validateUsername = async (value) => {
if (value.length < 3) {
return '用户名至少3个字符';
}
const isAvailable = await debouncedCheckUsername(value);
return isAvailable || '该用户名已被使用';
};
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input
{...register('username', {
required: '用户名不能为空',
validate: validateUsername,
})}
/>
{errors.username && <span>{errors.username.message}</span>}
<button type="submit">提交</button>
</form>
);
}复合异步验证
jsx
function ComplexAsyncValidation() {
const { register, handleSubmit, formState: { errors } } = useForm({
mode: 'onBlur',
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input
{...register('username', {
required: '用户名不能为空',
validate: {
// 同步验证
minLength: (value) =>
value.length >= 3 || '用户名至少3个字符',
noSpaces: (value) =>
!value.includes(' ') || '用户名不能包含空格',
// 异步验证
checkAvailability: async (value) => {
if (value.length < 3) return true; // 先通过同步验证
const response = await fetch(`/api/check-username?username=${value}`);
const data = await response.json();
return data.available || '该用户名已被使用';
},
},
})}
/>
{errors.username && <span>{errors.username.message}</span>}
<button type="submit">提交</button>
</form>
);
}验证时机
验证模式配置
jsx
function ValidationModes() {
// onSubmit - 仅在提交时验证
const form1 = useForm({ mode: 'onSubmit' });
// onBlur - 失焦时验证
const form2 = useForm({ mode: 'onBlur' });
// onChange - 值改变时验证
const form3 = useForm({ mode: 'onChange' });
// onTouched - 首次失焦后,后续改变时验证
const form4 = useForm({ mode: 'onTouched' });
// all - 失焦和改变时都验证
const form5 = useForm({ mode: 'all' });
// 重新验证模式
const form6 = useForm({
mode: 'onBlur',
reValidateMode: 'onChange', // 错误后的重新验证模式
});
}
// 实际应用示例
function ValidationModeExample() {
const {
register,
handleSubmit,
formState: { errors, touchedFields },
} = useForm({
mode: 'onBlur', // 失焦时验证
reValidateMode: 'onChange', // 有错误后改变时重新验证
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input
{...register('email', {
required: '邮箱不能为空',
pattern: {
value: /^\S+@\S+$/i,
message: '邮箱格式不正确',
},
})}
/>
{touchedFields.email && errors.email && (
<span>{errors.email.message}</span>
)}
<button type="submit">提交</button>
</form>
);
}手动触发验证
jsx
function ManualTrigger() {
const {
register,
handleSubmit,
trigger,
formState: { errors },
} = useForm({ mode: 'onBlur' });
const validateField = async (fieldName) => {
const isValid = await trigger(fieldName);
console.log(`${fieldName} is valid:`, isValid);
};
const validateMultiple = async () => {
const isValid = await trigger(['username', 'email']);
console.log('Fields are valid:', isValid);
};
const validateAll = async () => {
const isValid = await trigger();
console.log('Form is valid:', isValid);
};
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<div>
<input
{...register('username', {
required: '用户名不能为空',
})}
/>
<button
type="button"
onClick={() => validateField('username')}
>
验证用户名
</button>
{errors.username && <span>{errors.username.message}</span>}
</div>
<div>
<input
type="email"
{...register('email', {
required: '邮箱不能为空',
})}
/>
<button
type="button"
onClick={() => validateField('email')}
>
验证邮箱
</button>
{errors.email && <span>{errors.email.message}</span>}
</div>
<button type="button" onClick={validateMultiple}>
验证用户名和邮箱
</button>
<button type="button" onClick={validateAll}>
验证全部
</button>
<button type="submit">提交</button>
</form>
);
}错误显示
错误消息组件
jsx
function ErrorMessage({ error }) {
if (!error) return null;
return (
<div className="error-message">
<span className="error-icon">⚠</span>
<span>{error.message}</span>
</div>
);
}
// 使用示例
function FormWithErrorMessages() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<div className="form-field">
<input
{...register('username', {
required: '用户名不能为空',
minLength: {
value: 3,
message: '用户名至少3个字符',
},
})}
/>
<ErrorMessage error={errors.username} />
</div>
<button type="submit">提交</button>
</form>
);
}集中错误显示
jsx
function ErrorSummary({ errors }) {
const errorList = Object.entries(errors).map(([field, error]) => ({
field,
message: error.message,
}));
if (errorList.length === 0) return null;
return (
<div className="error-summary">
<h4>请修正以下错误:</h4>
<ul>
{errorList.map(({ field, message }) => (
<li key={field}>
<strong>{field}:</strong> {message}
</li>
))}
</ul>
</div>
);
}
// 使用示例
function FormWithErrorSummary() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<ErrorSummary errors={errors} />
<input
{...register('username', {
required: '用户名不能为空',
})}
/>
<input
type="email"
{...register('email', {
required: '邮箱不能为空',
})}
/>
<button type="submit">提交</button>
</form>
);
}实战案例
用户注册表单完整验证
jsx
function RegistrationFormWithValidation() {
const {
register,
handleSubmit,
watch,
formState: { errors, isSubmitting, isValidating },
} = useForm({
mode: 'onBlur',
reValidateMode: 'onChange',
});
const password = watch('password');
const checkUsername = async (username) => {
if (username.length < 3) return '用户名至少3个字符';
const response = await fetch(`/api/check-username?username=${username}`);
const data = await response.json();
return data.available || '该用户名已被使用';
};
const onSubmit = async (data) => {
try {
await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
alert('注册成功!');
} catch (error) {
alert('注册失败: ' + error.message);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="registration-form">
<h2>用户注册</h2>
<div className="form-group">
<label>用户名</label>
<input
{...register('username', {
required: '用户名不能为空',
minLength: {
value: 3,
message: '用户名至少3个字符',
},
maxLength: {
value: 20,
message: '用户名最多20个字符',
},
pattern: {
value: /^[a-zA-Z0-9_]+$/,
message: '只能包含字母、数字和下划线',
},
validate: checkUsername,
})}
/>
{isValidating && <span className="validating">验证中...</span>}
{errors.username && <span className="error">{errors.username.message}</span>}
</div>
<div className="form-group">
<label>邮箱</label>
<input
type="email"
{...register('email', {
required: '邮箱不能为空',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: '邮箱格式不正确',
},
})}
/>
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div className="form-group">
<label>手机号</label>
<input
{...register('phone', {
required: '手机号不能为空',
pattern: {
value: /^1[3-9]\d{9}$/,
message: '手机号格式不正确',
},
})}
/>
{errors.phone && <span className="error">{errors.phone.message}</span>}
</div>
<div className="form-group">
<label>密码</label>
<input
type="password"
{...register('password', {
required: '密码不能为空',
minLength: {
value: 8,
message: '密码至少8个字符',
},
pattern: {
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
message: '密码必须包含大小写字母和数字',
},
})}
/>
{errors.password && <span className="error">{errors.password.message}</span>}
</div>
<div className="form-group">
<label>确认密码</label>
<input
type="password"
{...register('confirmPassword', {
required: '请确认密码',
validate: (value) =>
value === password || '密码不匹配',
})}
/>
{errors.confirmPassword && <span className="error">{errors.confirmPassword.message}</span>}
</div>
<div className="form-group">
<label>年龄</label>
<input
type="number"
{...register('age', {
required: '年龄不能为空',
min: {
value: 18,
message: '必须年满18岁',
},
max: {
value: 100,
message: '年龄不能超过100岁',
},
valueAsNumber: true,
})}
/>
{errors.age && <span className="error">{errors.age.message}</span>}
</div>
<div className="form-group">
<label>
<input
type="checkbox"
{...register('agreeToTerms', {
required: '请同意服务条款',
})}
/>
我已阅读并同意服务条款
</label>
{errors.agreeToTerms && <span className="error">{errors.agreeToTerms.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '注册中...' : '注册'}
</button>
</form>
);
}总结
表单验证规则要点:
- 内置规则:required、minLength、maxLength、min、max、pattern
- 自定义验证:单个或多个validate函数
- 异步验证:支持async函数,可配合防抖
- 跨字段验证:通过watch或formValues访问其他字段
- 验证时机:onSubmit、onBlur、onChange、onTouched、all
- 错误显示:单独或集中显示错误信息
合理使用验证规则,能够确保数据质量并提供良好的用户体验。