如何消除异步表单/是的验证,当用户停止输入数据时它将进行验证?

问题描述 投票:0回答:4

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

到目前为止,这是我的表格。问题是它没有按预期工作。看来被谴责的函数返回了上一次调用的值,我不明白问题出在哪里。

您可以在这里看到一个实例: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 !== '[email protected]') {
                return resolve(true);
            } else {
                return resolve(false);
            }
        }, 200);
    })
}

更新: 尝试编写我自己的去抖动功能的实现。这样,最后一个 Promise 的解析将被保留到超时,然后函数才会被调用,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

reactjs formik yup
4个回答
5
投票

看起来被谴责的函数返回了先前调用的值

这就是 lodash debounce 的工作原理:

对去抖函数的后续调用将返回最后一次 func 调用的结果。

参见: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 (
    ...
  );
}

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

如果没有依赖关系,最好将模式放在组件之外,在每次渲染中执行此操作通常很慢。


2
投票

如果您不希望所有验证都被反跳(仅字段的异步验证),实现此目的的另一种方法是利用自定义更改处理程序和

setFieldError

如果您想在验证时阻止提交,可以使用

setStatus

示例

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

const debouncedEmailValidation = debounce((val, setFieldError) => {
  isEmailExists(val)
    .then(res => {
      if (res.data.exists) {
        setFieldError('email', 'Email already exists');
      }
    });
}, 300, {
  trailing: true
});

const validationSchema = yup.object({
    email: yup
      .string()
      .required()
      .email()
  });

export default function App() {
  return (
    <Formik
      initialValues={{
        email: ""
      }}
      validationSchema={validationSchema}
      onSubmit={console.log}
    >
      {({ values, errors, handleBlur, handleChange, setFieldError }) => {
        const handleEmailChange = (e) => {
          handleChange(e);
          debouncedEmailValidation(e.target.value, setFieldError);
        };

        return (
          <Form>
            <label>
              Email:
              <input
                type="text"
                name="email"
                onChange={handleEmailChange}
                onBlur={handleBlur}
                value={values.email}
              />
              <div className="error-message">{errors.email}</div>
            </label>
          </Form>
        )
      }}
    </Formik>
  );
}

1
投票

如果你想使用 < 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} //  <------
        ...


0
投票

在我的例子中,带有表单的组件不会重新渲染,并且这个 useEffect 不起作用。 有人知道为什么会这样吗?字段值更新。 我已经完成了最后一条评论。

export const CreditForm: React.FC = () => {
  const [selectData, setSelectData] = useState(initialSelectorsData);
  const [successfulSubmit, setSuccessfulSubmit] = useState(false);

  useEffect(() => {
    const getCreditFormData = async () => {
      const data = await creditFormApi.getDateForCreditForm();
      setSelectData(data);
    };
    getCreditFormData();
  }, []);

  // const formik = useRef<HTMLFormElement | null>(null);
  const formik = useRef(null);
  const debouncedValidate = useMemo(
    () => debounce(() => formik.current?.validateForm, 500),
    [formik]
  );

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

  console.log(formik.current?.values);

  console.log({ formik });

  return (
    <div className="flex items-center">
      {!successfulSubmit ? (
        <Formik
          initialValues={initialFormData}
          validationSchema={validationSchema}
          validateOnBlur={false}
          validateOnChange={false}
          innerRef={formik}
          onSubmit={async values => {
            try {
              await creditFormApi.submitForm(values);
              setSuccessfulSubmit(true);
            } catch (error) {
              alert('Something went wrong');
            }
          }}
        >
          {({
            values,
            setFieldValue,
            isSubmitting,
            isValid,
            isValidating,
          }: FormikProps<IInitialValues>) => (
            <Form className="flex flex-col md:flex-row w-full rounded-lg overflow-hidden">
              <div className={cn(formColumnStyles, 'md:py-32 bg-[#fff]')}>
                <h2 className="text-2xl md:text-3xl lg:text-4xl font-extrabold text-center mb-3">
                  Submit External Credit
                </h2>
                <p className="text-center mb-3 font-medium">Enter your credit details below</p>
                <InputField
                  name="email"
                  placeholder="Email"
                  checkIcon={true}
                  isValidating={isValidating}
                  asyncValidating={true}
                />
              </div>
            </Form>
          )}
        </Formik>
      ) : (
        <SuccessfulSubmit />
      )}
    </div>
  );
};

© www.soinside.com 2019 - 2024. All rights reserved.