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
相关产品推荐
相关产品推荐

