如何为Formik/Yup异步校验添加防抖,使其在用户停止输入后触发
正确实现防抖异步校验的方案
问题原因
- lodash 自带的
debounce不处理异步函数的 Promise 返回,被防抖拦截的调用返回的 Promise 会一直 pending 或者返回旧的执行结果 - Yup 的异步 test 每次触发新校验时,会自动忽略上一次未返回的校验结果,即便后续旧请求返回也不会更新表单状态
- 你自定义的防抖函数逻辑错误,每次新调用都会主动触发前一次的 resolve 逻辑,导致返回的结果不符合预期
推荐实现方案
方案1:使用正确的异步防抖函数+Yup test
首先实现一个支持 Promise 处理的防抖函数,确保只有最后一次调用会触发实际的 API 请求,且所有 pending 的 Promise 都能拿到最新的结果:
import { useMemo } from 'react'; import { isEmailExists } from "./api"; import * as yup from 'yup'; import { useFormik } from 'formik'; // 正确的异步防抖函数 const debounceAsync = (func, wait) => { let timeoutId; let latestResolve; return (...args) => { // 清除上一次的定时器 clearTimeout(timeoutId); // 返回新的Promise return new Promise((resolve) => { // 保存最新的resolve方法 latestResolve = resolve; timeoutId = setTimeout(async () => { const result = await func(...args); latestResolve(result); }, wait); }); }; }; export default function App() { // 用useMemo缓存防抖后的API调用,避免组件重渲染时生成新的防抖实例 const debouncedCheckEmail = useMemo(() => { return debounceAsync(isEmailExists, 300); }, []); const validationSchema = yup.object({ email: yup .string() .required("邮箱不能为空") .email("请输入合法的邮箱格式") .test("unique_email", "邮箱已被注册", async (email) => { // 空值已经被required校验拦截,这里直接返回true跳过 if (!email) return true; const isExists = await debouncedCheckEmail(email); // API返回true代表邮箱存在,所以校验不通过返回false,反之返回true return !isExists; }) }); const formik = useFormik({ initialValues: { email: "" }, validateOnMount: false, validationSchema: validationSchema, onSubmit: async (values, actions) => { // 提交前可以主动触发一次校验,避免防抖还没执行的情况 const isValid = await validationSchema.validate(values); if (isValid) { // 处理提交逻辑 } } }); return ( <form onSubmit={formik.handleSubmit}> <label> Email: <input type="text" name="email" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.email} /> {formik.touched.email && formik.errors.email && ( <div className="error-message">{formik.errors.email}</div> )} </label> <button type="submit">提交</button> </form> ); }
方案2:将防抖逻辑移到Formik validate层面
如果你觉得在Yup test里处理防抖不方便,可以直接用Formik的异步validate属性配合防抖,逻辑更直观:
import { useCallback } from 'react'; import { debounce } from 'lodash'; import { isEmailExists } from "./api"; import { useFormik } from 'formik'; import * as yup from 'yup'; export default function App() { // 同步校验逻辑用Yup处理 const validationSchema = yup.object({ email: yup .string() .required("邮箱不能为空") .email("请输入合法的邮箱格式") }); // 防抖处理异步邮箱校验 const debouncedCheckEmail = useCallback( debounce(async (email, setFieldError) => { if (!email) return; const isExists = await isEmailExists(email); if (isExists) { setFieldError('email', '邮箱已被注册'); } }, 300), [] ); const formik = useFormik({ initialValues: { email: "" }, validationSchema: validationSchema, validate: async (values) => { // 每次输入变化触发防抖校验 debouncedCheckEmail(values.email, formik.setFieldError); }, onSubmit: async (values, actions) => { // 提交前主动调用一次校验,避免防抖延迟导致的状态未更新 const isExists = await isEmailExists(values.email); if (isExists) { actions.setFieldError('email', '邮箱已被注册'); return; } // 处理提交逻辑 } }); return ( <form onSubmit={formik.handleSubmit}> <label> Email: <input type="text" name="email" onChange={formik.handleChange} onBlur={formik.handleBlur} value={formik.values.email} /> {formik.touched.email && formik.errors.email && ( <div className="error-message">{formik.errors.email}</div> )} </label> <button type="submit">提交</button> </form> ); }
注意事项
- 防抖后的函数必须用
useMemo或者useCallback缓存,避免组件每次重渲染生成新的防抖实例,导致防抖失效 - 表单提交前必须主动触发一次异步校验,避免用户输入完成后立刻点击提交,此时防抖定时器还没触发,校验状态未更新的问题
- API返回结果处理要注意逻辑对应:如果返回
true代表邮箱已存在,那Yup的test函数需要返回false才会触发错误提示
内容的提问来源于stack exchange,提问作者Anatoly
相关产品推荐
相关产品推荐

