如何去抖动异步 formik/yup 验证,当用户停止输入数据时它会验证?

How to debounce async formik/yup validation, that it will validate when user will stop entering data?

我想异步验证用户输入。例如,检查电子邮件是否已存在,并在用户输入时执行验证。为了减少 API 次调用,我想使用 lodash 或自定义去抖动函数去抖动 API 次调用,并在用户停止输入时执行验证。

到目前为止,这是我现在的表格。问题是它没有按预期工作。看起来谴责函数 returns 是上一次调用的值,我无法理解问题出在哪里。

你可以在这里看到一个活生生的例子:https://codesandbox.io/s/still-wave-qwww6

import { isEmailExists } from "./api";

const debouncedApi = _.debounce(isEmailExists, 300, {
  trailing: true
});

export default function App() {
  const validationSchema = yup.object({
    email: yup
      .string()
      .required()
      .email()
      .test("unique_email", "Email must be unique", async (email, values) => {
        const response = await debouncedApi(email);
        console.log(response);
        return response;
      })
  });

  const formik = useFormik({
    initialValues: {
      email: ""
    },
    validateOnMount: true,
    validationSchema: validationSchema,
    onSubmit: async (values, actions) => {}
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <label>
        Email:
        <input
          type="text"
          name="email"
          onChange={formik.handleChange}
          onBlur={formik.handleBlur}
          value={formik.values.email}
        />
        <div className="error-message">{formik.errors.email}</div>
      </label>
    </form>
  );
}

我使用以下函数模拟 API 调用:

export const isEmailExists = async email => {
    return new Promise(resolve => {
        console.log('api call', email);
        setTimeout(() => {
            if (email !== 'test@gmail.com') {
                return resolve(true);
            } else {
                return resolve(false);
            }
        }, 200);
    })
}

更新: 试图编写我自己的去抖功能实现。这样,最后一个 Promise 的 resolve 将一直保留到超时到期,然后才会调用函数并解析 Promise。

const debounce = func => {
    let timeout;
    let previouseResolve;
    return function(query) {
         return new Promise(async resolve => {

            //invoke resolve from previous call and keep current resolve
            if (previouseResolve) {
                const response = await func.apply(null, [query]);
                previouseResolve(response);
            }
            previouseResolve = resolve;

            //extending timeout
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            timeout = setTimeout(async () => {
                const response = await func.apply(null, [query]);
                console.log('timeout expired', response);
                previouseResolve(response);
                timeout = null;
            }, 200);
        })
    }
}

const debouncedApi = debounce(isEmailExists);

 const validationSchema = yup.object({
        email: yup
            .string()
            .required()
            .email()
            .test('unique_email', 'Email must be unique', async (email, values) => {
                const response = await debouncedApi(email);
                console.log('test response', response);
                return response;
            })
    });

不幸的是,它也不起作用。看起来是的,当下一次调用发生时,中止未解析的函数调用。当我打字快时它不起作用,当我打字慢时它起作用。 您可以在此处查看更新的示例:https://codesandbox.io/s/suspicious-chaum-0psyp

It looks that denounced function returns a value from the previous call

这就是 lodash 去抖的工作原理:

Subsequent calls to the debounced function return the result of the last func invocation.

参见:https://lodash.com/docs/4.17.15#debounce

您可以将 validateOnChange 设置为 false,然后手动调用 formik.validateForm 作为副作用:

import debounce from 'lodash/debounce';
import { isEmailExists } from "./api";

const validationSchema = yup.object({
  email: yup
    .string()
    .required()
    .email()
    .test("unique_email", "Email must be unique", async (email, values) => {
      const response = await isEmailExists(email);
      console.log(response);
      return response;
    })
});

export default function App() {
  const formik = useFormik({
    initialValues: {
      email: ""
    },
    validateOnMount: true,
    validationSchema: validationSchema,
    validateOnChange: false, // <--
    onSubmit: async (values, actions) => {}
  });

  const debouncedValidate = useMemo(
    () => debounce(formik.validateForm, 500),
    [formik.validateForm],
  );

  useEffect(
    () => {
      console.log('calling deboucedValidate');
      debouncedValidate(formik.values);
    },
    [formik.values, debouncedValidate],
  );

  return (
    ...
  );
}

这样,整个验证将被去抖,而不仅仅是远程调用。

如果没有依赖关系,最好将架构放在组件之外,在每次渲染时这样做通常很慢。

如果你想使用 < Formik > 组件(像我一样),你可以像这样去抖动验证(感谢之前的回答,它帮助我做到这一点):

import { Formik, Form, Field } from "formik"
import * as Yup from 'yup';
import { useRef, useEffect, useMemo } from 'react'
import debounce from 'lodash.debounce'


const SignupSchema = Yup.object().shape({
    courseTitle: Yup.string().min(3, 'Too Short!').max(200, 'Too Long!').required('Required'),
    courseDesc: Yup.string().min(3, 'Too Short!').required('Required'),
    address: Yup.string().min(3, 'Too Short!').max(200, 'Too Long!').required('Required'),
});

export default function App() {
    const formik = useRef() //  <------
    const debouncedValidate = useMemo(
        () => debounce(() => formik.current?.validateForm, 500),
        [formik],
    );

    useEffect(() => {
        console.log('calling deboucedValidate');
        debouncedValidate(formik.current?.values);
    }, [formik.current?.values, debouncedValidate]);

    return (
      <Formik
        innerRef={formik} //  <------
        initialValues={{
            courseTitle: '',
            courseDesc: '',
            address: '',
        }}
        validationSchema={SignupSchema}
        validateOnMount={true} //  <------
        validateOnChange={false} //  <------
        ...