如何去抖动异步 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} // <------
...
我想异步验证用户输入。例如,检查电子邮件是否已存在,并在用户输入时执行验证。为了减少 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} // <------
...