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). 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. You can clone the repo, run npm i , npm start, and you will see the problem when you change the form. 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
UPDATE: thanks so much for the answer. I took your answer and abstracted away a lot of the debounce/callback logic in a custom hook (not typed yet):
import React from 'react'
import debounce from 'just-debounce-it'
export function useDebouncedAutoSave({
mutationResult,
validationSchema,
getValues,
reset,
queryResult
}: {
mutationResult: any
validationSchema: any
getValues: any
reset: any
queryResult: any
}) {
React.useEffect(() => {
reset(queryResult.data)
}, [queryResult.data])
const handleDebouncedChange = React.useMemo(
() =>
debounce((data: any) => {
mutationResult.mutateAsync(data)
}, 500),
[mutationResult.mutateAsync]
)
const onChange = async () => {
try {
const values = getValues()
const validated = await validationSchema.validate(values)
if (!validated) return
handleDebouncedChange(validated)
} catch (e) {}
}
return {
onChange
}
}