简体   繁体   English

如何使用 React-Hook-Form 自动保存进行 React-Query

[英]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.大多数时候,我依赖onChangeonBlur表单事件。

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM