人人都会AI编程

16.2 VeeValidate:声明式表单校验方案

更新时间:2026-07-10

在实际开发中,表单往往伴随着复杂的状态管理(字段值、错误信息、触摸状态、提交状态)和繁琐的校验逻辑。Formik 与 Yup 的组合提供了一套声明式、可组合、易维护的表单解决方案,让开发者从手动维护表单状态的苦恼中解脱出来。

Formik 的核心能力

Formik 是一个轻量级的表单状态管理库,它帮你处理:

  • 字段值的收集与更新
  • 表单校验与错误信息管理
  • 字段的访问(脏状态)与触摸状态(touched)
  • 提交处理与重置
  • 尽量减少不必要的重渲染(通过 Field 组件或 useField Hook)

Formik 本质上是一个“受控表单的自动化外壳”,你不再需要为每个输入框编写单独的 useState 和 onChange 处理器。

Yup 的角色:声明式校验

Yup 是一种基于 schema 的校验库,允许你用链式语法描述每个字段的校验规则,生成校验器。它和 Formik 天然集成,Formik 直接接受 Yup schema 作为 validationSchema,自动完成校验并填充错误对象。

快速上手:一个完整的登录表单

import { Formik, Field, Form, ErrorMessage } from 'formik';
import * as Yup from 'yup';

// 1. 定义校验 schema(声明式校验)
const loginSchema = Yup.object({
  email: Yup.string()
    .email('请输入有效的邮箱地址')
    .required('邮箱不能为空'),
  password: Yup.string()
    .min(6, '密码至少6位')
    .required('密码不能为空'),
});

// 2. 使用 Formik 组件
function LoginForm() {
  const handleSubmit = (values, { setSubmitting }) => {
    // 模拟登录请求
    console.log('提交数据:', values);
    setTimeout(() => {
      alert('登录成功');
      setSubmitting(false);
    }, 1000);
  };

  return (
    <Formik
      initialValues={{ email: '', password: '' }}
      validationSchema={loginSchema}
      onSubmit={handleSubmit}
    >
      {({ isSubmitting }) => (
        <Form>
          <div>
            <label htmlFor="email">邮箱</label>
            <Field type="email" name="email" />
            <ErrorMessage name="email" component="div" className="error" />
          </div>

          <div>
            <label htmlFor="password">密码</label>
            <Field type="password" name="password" />
            <ErrorMessage name="password" component="div" className="error" />
          </div>

          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? '登录中...' : '登录'}
          </button>
        </Form>
      )}
    </Formik>
  );
}

代码解析:

  • validationSchema={loginSchema}:当你输入时,Formik 自动运行 Yup 校验,发现错误后立即阻止提交并显示错误信息。
  • <Field> 组件自动绑定 value、onChange、onBlur,并标记触摸状态。
  • <ErrorMessage> 仅在该字段被触摸且存在错误时渲染,避免页面初始就显示一堆红色提示。
  • isSubmitting 由 Formik 在 onSubmit 执行时自动设置为 true,返回 Promise 结束后恢复,防止重复提交。

进阶用法:自定义组件与 useField Hook

对于自定义 UI 组件(如设计系统中的 TextInput),可以使用 useField Hook 将其无缝集成进 Formik:

import { useField } from 'formik';

function MyTextInput({ label, ...props }) {
  const [field, meta] = useField(props);
  return (
    <div>
      <label htmlFor={props.id || props.name}>{label}</label>
      <input {...field} {...props} />
      {meta.touched && meta.error ? (
        <div className="error">{meta.error}</div>
      ) : null}
    </div>
  );
}

// 使用
<Formik initialValues={{ name: '' }} ...>
  <Form>
    <MyTextInput label="用户名" name="name" />
  </Form>
</Formik>

这样所有表单字段都可以复用统一的样式和行为,错误提示也完全由 Formik 状态驱动。

Yup 的强大校验能力

Yup 支持丰富的数据类型校验,甚至包括自定义校验:

const schema = Yup.object({
  age: Yup.number()
    .typeError('必须是数字')
    .min(18, '至少18岁')
    .required('必填'),
  confirmPassword: Yup.string()
    .oneOf([Yup.ref('password'), null], '密码不一致')
    .required('请确认密码'),
  tags: Yup.array().min(1, '至少选择一个标签'),
  website: Yup.string().url('请输入合法网址').nullable(),
});

跨字段校验(例如密码确认)通过 Yup.ref 轻松实现。

对比 React Hook Form

| 特性 | Formik + Yup | React Hook Form |
|------|--------------|-----------------|
| 编程范式 | 声明式(JSX 模板) | 声明式 + Hook |
| 性能 | 每次输入会触发顶层 Formik 重渲染(可优化) | 默认非受控,极低重渲染 |
| 上手难度 | 容易,传统 React 思维 | 需要理解 register 机制 |
| 校验集成 | Yup 原生支持 | Yup 通过 resolver 集成 |
| 体积 | 约 15 kB gzip | 约 10 kB gzip |
| 适用场景 | 中小型表单,偏好声明式写法 | 大型复杂表单,追求极致性能 |

如果你团队更习惯“组件即一切”的 React 风格,Formik 会让表单逻辑保持可读性;当表单包含几十上百个字段且需要极致性能时,React Hook Form 是更优选择。

常见坑点与最佳实践

  1. 避免匿名函数作为 validationSchema 参数:

如果每次渲染都重新创建 schema,会导致 Formik 内部的 diff 失效。将 schema 定义在组件外部,或用 useMemo 包裹。

  1. 字段名与嵌套结构:

Formik 支持点号路径 "address.city",Yup 也能描述嵌套对象校验,保持一致性。

  1. 异步校验:

Yup schema 支持 .test() 方法进行异步校验(如检查用户名是否已存在),但需要配合 Formik 的 validate 回调进行 debounce,避免频繁请求。

  1. 减少不必要的重渲染:

可以使用 FastField 替代 Field,它会自动浅比较 props 和 value,避免无关字段变化引起的重渲染。

总结

Formik + Yup 是一套成熟稳定的表单方案,它把表单状态、校验、提交过程全面声明化,让表单代码从繁杂的样板逻辑中解放出来,变得清晰易读。在中小型到中大型项目中,它能显著提升开发效率和代码可维护性,是除 React Hook Form 之外最值得掌握的 React 表单技能。