Skip to content

表单验证规则

概述

表单验证是确保数据质量和用户体验的关键环节。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>
  );
}

总结

表单验证规则要点:

  1. 内置规则:required、minLength、maxLength、min、max、pattern
  2. 自定义验证:单个或多个validate函数
  3. 异步验证:支持async函数,可配合防抖
  4. 跨字段验证:通过watch或formValues访问其他字段
  5. 验证时机:onSubmit、onBlur、onChange、onTouched、all
  6. 错误显示:单独或集中显示错误信息

合理使用验证规则,能够确保数据质量并提供良好的用户体验。