[英]how to make a React-Query with React-Hook-Form Autosave
I am trying to make a form with React-Hook-Form and React-Query that autosaves whenever the user changes any of the fields (debounced).我正在尝试使用 React-Hook-Form 和 React-Query 制作一个表单,只要用户更改任何字段(去抖动),它就会自动保存。 I am getting close, but it creates an infinite loop when I mutate.
我越来越接近了,但是当我变异时它会创建一个无限循环。 Here is what I have:
这是我所拥有的:
"@tanstack/react-query": "^4.2.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.34.2",
import React from 'react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import * as Yup from 'yup'
import debounce from 'just-debounce-it'
type NameType = {
id: number
firstName: string
lastName: string
}
const schemaValidation = Yup.object().shape({
id: Yup.number().required('Required'),
firstName: Yup.string().required('Required'),
lastName: Yup.string()
.min(2, 'Must be greater than 1 character')
.max(50, 'Must be less than 50 characters')
})
const getMockData = async () => {
const name: NameType = {
id: 1,
firstName: 'John',
lastName: 'Doe'
}
return await Promise.resolve(name)
}
const saveChangeToDatabase = async (args: NameType) => {
console.count('payload for patch:' + JSON.stringify(args))
return await Promise.resolve(args)
}
const NameForm = () => {
const queryResult = useQuery(['user'], getMockData)
const mutationResult = useMutation(saveChangeToDatabase, {
onSuccess: (nameToSave: NameType) => {
console.count('success mutating: ' + JSON.stringify(nameToSave))
}
})
const {
register,
reset,
watch,
formState: { isValid, isDirty, errors }
} = useForm<NameType>({
mode: 'all',
criteriaMode: 'all',
resolver: yupResolver(schemaValidation)
})
const fieldData = watch()
const handleDebouncedChange = debounce((data: NameType) => {
mutationResult.mutateAsync(data)
}, 500)
React.useEffect(() => {
reset(queryResult.data)
}, [queryResult.data])
React.useEffect(() => {
if (isValid && isDirty) {
handleDebouncedChange(fieldData)
}
}, [fieldData, isValid, isDirty])
if (queryResult.isLoading) {
return <h2>Loading...</h2>
}
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
margin: 'auto',
width: 300
}}>
<input {...register('firstName')} placeholder='First name' />
<div style={{ color: 'red' }}>{errors && errors?.firstName?.message}</div>
<input {...register('lastName')} placeholder='Last name' />
<div style={{ color: 'red' }}>{errors && errors?.lastName?.message}</div>
{'Field data: ' + JSON.stringify(fieldData)}
</div>
)
}
export default NameForm
I also made a create-react-app reproduction of this here.我还在这里做了一个 create-react-app 复制。 You can clone the repo, run npm i, npm start, and you will see the problem when you change the form.
你可以克隆repo,运行npm i,npm启动,当你改变表单时你会看到问题。 This is the only page you need to look at:
这是您需要查看的唯一页面:
https://github.com/k-38/react-query_react-hook-form_autosave/blob/main/src/NameForm.tsx
Any help is appreciated, thank you任何帮助表示赞赏,谢谢
So, the infinite loop in functional component with useEffect
is often due to dependencies values mutated on every "loop cycle".因此,具有
useEffect
的功能组件中的无限循环通常是由于每个“循环周期”上的依赖值都发生了变化。
In the documentation we can read:在文档中,我们可以阅读:
watch result is optimised for render phase instead of useEffect's deps, to detect value update you may want to use an external custom hook for value comparison.
watch 结果针对渲染阶段而不是 useEffect 的 deps 进行了优化,为了检测值更新,您可能需要使用外部自定义挂钩进行值比较。
I suspect (didn't had time to look at the code) that watch
return value is created on every render.我怀疑(没有时间查看代码)在每次渲染时都会创建
watch
返回值。 Then fieldData
on every render is a reference to a new object.然后每个渲染上的
fieldData
都是对新 object 的引用。
Most of the time, I rely on onChange
or onBlur
form events.大多数时候,我依赖
onChange
或onBlur
表单事件。
function Form() {
const onChangeHandler = () => { /* ... */ };
return <form onChange={onChangeHandler}>
{/* ... */}
</form>
}
And then I use useForm().getValues
function to retrieve the current form values, but you need to use you schema validation to trigger the "autosave" function when its valid.然后我使用
useForm().getValues
function 来检索当前的表单值,但是您需要使用架构验证来触发“自动保存” function 在其有效时。
Another solution (maybe a simpler solution): would be to use a custom hook that compare the values deeply.另一种解决方案(可能是更简单的解决方案):将使用自定义挂钩来深入比较这些值。 You can take a look at
useDeepCompareEffect
from react-use for this.您可以查看react-use中的
useDeepCompareEffect
。
There is another bug in your code with the debounce function: using const debouncedFunction = debounce(myFunction, 500)
will not work.您的代码中还有另一个错误,即 debounce function:使用
const debouncedFunction = debounce(myFunction, 500)
将不起作用。 A "debounced" function is memoized. “去抖” function 被记忆。 As we are in a render function (functional component), the memoized function will be created on every render, so it will be called without respect to the threshold that you set.
由于我们在渲染 function(功能组件)中,因此将在每次渲染时创建记忆的 function,因此将在不考虑您设置的阈值的情况下调用它。
You need to use React.useMemo
for this:您需要为此使用
React.useMemo
:
const { mutateAsync } = mutationResult;
const handleDebouncedChange = React.useMemo(
() =>
debounce((data: NameType) => {
mutateAsync(data);
}, 500),
[mutateAsync]
);
A fully working version available here as codesandbox would be: 此处作为代码沙盒可用的完整工作版本将是:
import React from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as Yup from "yup";
import debounce from "just-debounce-it";
type NameType = {
id: number;
firstName: string;
lastName: string;
};
const schemaValidation = Yup.object().shape({
id: Yup.number().required("Required"),
firstName: Yup.string().required("Required"),
lastName: Yup.string()
.required()
.min(2, "Must be greater than 1 character")
.max(50, "Must be less than 30 characters")
});
const getMockData = async () => {
const name: NameType = {
id: 1,
firstName: "John",
lastName: "Doe"
};
return await Promise.resolve(name);
};
const saveChangeToDatabase = async (args: NameType) => {
console.count("payload for patch:" + JSON.stringify(args));
return await Promise.resolve(args);
};
const NameForm = () => {
const queryResult = useQuery(["user"], getMockData);
const mutationResult = useMutation(saveChangeToDatabase, {
onSuccess: (nameToSave: NameType) => {
console.count("success mutating: " + JSON.stringify(nameToSave));
}
});
const { register, reset, watch, getValues, formState } = useForm<NameType>({
mode: "all",
criteriaMode: "all",
resolver: yupResolver(schemaValidation)
});
const { errors } = formState;
const fieldData = watch();
const { mutateAsync } = mutationResult;
const handleDebouncedChange = React.useMemo(
() =>
debounce((data: NameType) => {
mutateAsync(data);
}, 500),
[mutateAsync]
);
React.useEffect(() => {
reset(queryResult.data);
}, [queryResult.data]);
const onChange = async () => {
const data = getValues();
try {
console.log(formState);
const validated = await schemaValidation.validate(data);
handleDebouncedChange(validated);
} catch (e) {}
};
if (queryResult.isLoading) {
return <h2>Loading...</h2>;
}
return (
<form
style={{
display: "flex",
flexDirection: "column",
margin: "auto",
width: 300
}}
onChange={onChange}
>
<input {...register("firstName")} placeholder="First name" />
<div style={{ color: "red" }}>{errors && errors?.firstName?.message}</div>
<input {...register("lastName")} placeholder="Last name" />
<div style={{ color: "red" }}>{errors && errors?.lastName?.message}</div>
{"Field data: " + JSON.stringify(fieldData)}
</form>
);
};
export default NameForm;
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.