You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Formik Yup自定义.test()验证异常:单个字段校验触发全局校验

解决Formik+Yup异步校验触发异常的问题

问题根源

默认情况下,Formik结合Yup使用时,只要表单任意字段发生变化,就会触发整个Schema的全量校验,包括所有字段的同步/异步校验规则。这就是为什么切换username或password时,email的异步checkEmail会被莫名调用——因为每次字段变动都重新跑了一遍完整的Schema校验。

解决方案

提供三种可行的解决思路,按需选择:

方案1:改用Formik字段级自定义校验

放弃全量Schema校验,转而在Formik的validate函数中针对单个字段做校验,仅在字段被触摸(touched)或值变化时触发对应异步请求:

import { useFormik } from 'formik';
import * as Yup from 'yup';

// 单独定义各字段的同步校验规则
const emailSyncSchema = Yup.string()
  .email('请输入合法邮箱')
  .required('邮箱不能为空');

const usernameSyncSchema = Yup.string()
  .min(3, '用户名至少3位')
  .required('用户名不能为空');

const formik = useFormik({
  initialValues: { email: '', username: '', password: '' },
  validate: async (values) => {
    const errors = {};

    // 仅当email被触摸且有值时,执行异步校验
    if (formik.touched.email && values.email) {
      // 先做同步校验
      try {
        await emailSyncSchema.validate(values.email);
      } catch (err) {
        errors.email = err.message;
      }
      // 再做异步校验(如果同步校验通过)
      if (!errors.email) {
        const exists = await AuthenticationService.checkEmail(values.email);
        if (exists) errors.email = '邮箱已存在';
      }
    }

    // username同理
    if (formik.touched.username && values.username) {
      try {
        await usernameSyncSchema.validate(values.username);
      } catch (err) {
        errors.username = err.message;
      }
      if (!errors.username) {
        const exists = await AuthenticationService.checkUsername(values.username);
        if (exists) errors.username = '用户名已存在';
      }
    }

    // password的同步校验直接处理
    if (!values.password) {
      errors.password = '密码不能为空';
    } else if (values.password.length < 6) {
      errors.password = '密码至少6位';
    }

    return errors;
  },
  onSubmit: (values) => {
    // 提交逻辑
  },
});

方案2:保留Schema但限制异步校验触发条件

在Yup的.test方法中添加判断,仅当字段值非空且与初始值不同时,才执行异步请求。同时需要将初始值传入Schema上下文:

import { useFormik } from 'formik';
import * as Yup from 'yup';

const formik = useFormik({
  initialValues: { email: '', username: '', password: '' },
  validationSchema: Yup.object({
    email: Yup.string()
      .email('请输入合法邮箱')
      .required('邮箱不能为空')
      .test(
        'email-exists',
        '邮箱已存在',
        async (value, context) => {
          // 仅当值非空且与初始值不同时,调用接口
          if (!value || value === context.parent.initialEmail) return true;
          const exists = await AuthenticationService.checkEmail(value);
          return !exists;
        }
      ),
    username: Yup.string()
      .min(3, '用户名至少3位')
      .required('用户名不能为空')
      .test(
        'username-exists',
        '用户名已存在',
        async (value, context) => {
          if (!value || value === context.parent.initialUsername) return true;
          const exists = await AuthenticationService.checkUsername(value);
          return !exists;
        }
      ),
    password: Yup.string()
      .min(6, '密码至少6位')
      .required('密码不能为空'),
  }),
  // 传递初始值到Schema上下文
  validateSchema: (schema, values) => {
    return schema.validate({
      ...values,
      initialEmail: formik.initialValues.email,
      initialUsername: formik.initialValues.username,
    }, { abortEarly: false });
  },
  onSubmit: (values) => {
    // 提交逻辑
  },
});

方案3:手动控制字段校验时机

关闭Formik全局的自动校验,在单个字段的onChange/onBlur事件中手动触发字段级校验:

import { useFormik, Field } from 'formik';
import * as Yup from 'yup';

const validationSchema = Yup.object({
  email: Yup.string()
    .email('请输入合法邮箱')
    .required('邮箱不能为空')
    .test('email-exists', '邮箱已存在', async (value) => {
      if (!value) return true;
      const exists = await AuthenticationService.checkEmail(value);
      return !exists;
    }),
  username: Yup.string()
    .min(3, '用户名至少3位')
    .required('用户名不能为空')
    .test('username-exists', '用户名已存在', async (value) => {
      if (!value) return true;
      const exists = await AuthenticationService.checkUsername(value);
      return !exists;
    }),
  password: Yup.string()
    .min(6, '密码至少6位')
    .required('密码不能为空'),
});

const formik = useFormik({
  initialValues: { email: '', username: '', password: '' },
  validationSchema,
  validateOnChange: false, // 关闭全局 onChange 自动校验
  validateOnBlur: false, // 关闭全局 onBlur 自动校验
  onSubmit: (values) => {
    // 提交逻辑
  },
});

// 组件中使用Field时手动触发校验
return (
  <form onSubmit={formik.handleSubmit}>
    <div>
      <label>邮箱</label>
      <Field
        name="email"
        onChange={(e) => {
          formik.handleChange(e);
          // 仅在email变化时触发该字段的校验
          formik.validateField('email');
        }}
        onBlur={(e) => {
          formik.handleBlur(e);
          formik.validateField('email');
        }}
      />
      {formik.touched.email && formik.errors.email && <div>{formik.errors.email}</div>}
    </div>

    {/* username和password字段同理 */}
    <div>
      <label>用户名</label>
      <Field
        name="username"
        onChange={(e) => {
          formik.handleChange(e);
          formik.validateField('username');
        }}
        onBlur={(e) => {
          formik.handleBlur(e);
          formik.validateField('username');
        }}
      />
      {formik.touched.username && formik.errors.username && <div>{formik.errors.username}</div>}
    </div>

    <button type="submit">提交</button>
  </form>
);

注意事项

  • 方案1灵活性最高,能完全控制每个字段的校验时机和顺序;
  • 方案2适合不想改动太多现有Schema的场景,但需注意初始值的传递;
  • 方案3需要逐个字段配置事件,适合表单字段较少的情况;
  • 无论哪种方案,都建议给异步校验添加loading状态,避免重复请求。

内容的提问来源于stack exchange,提问作者Ganzo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 23:45:36