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

如何为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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 03:54:04