人人都会AI编程

16.2 Formik + Yup:声明式表单与校验方案

更新时间:2026-07-10

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

Formik 的核心能力

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

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

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

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> 组件自动绑定 valueonChangeonBlur,并标记触摸状态。
  • <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 表单技能。