如何使用 React-Hook-Form Autosave 进行 React-Query

Jar*_*red 4 javascript reactjs react-hook-form react-query

我正在尝试使用 React-Hook-Form 和 React-Query 制作一个表单,只要用户更改任何字段(去抖),它就会自动保存。我已经很接近了,但是当我变异时它会产生无限循环。这是我所拥有的:

"@tanstack/react-query": "^4.2.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.34.2",
Run Code Online (Sandbox Code Playgroud)
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

Run Code Online (Sandbox Code Playgroud)

我还在这里制作了一个 create-react-app 复制品。您可以克隆存储库,运行 npm i 、 npm start ,当您更改表单时您就会看到问题。这是您需要查看的唯一页面:

https://github.com/k-38/react-query_react-hook-form_autosave/blob/main/src/NameForm.tsx
Run Code Online (Sandbox Code Playgroud)

任何帮助表示感谢,谢谢

更新:非常感谢您的回答。我接受了你的答案,并在自定义挂钩中抽象出了许多去抖/回调逻辑(尚未输入):

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
    }
}


Run Code Online (Sandbox Code Playgroud)

gpi*_*hot 5

因此,功能组件中的无限循环useEffect通常是由于每个“循环周期”上的依赖值发生突变所致。

文档中我们可以读到:

watch 结果针对渲染阶段而不是 useEffect 的 deps 进行了优化,为了检测值更新,您可能需要使用外部自定义挂钩进行值比较。

我怀疑(没有时间查看代码)watch每个渲染都会创建返回值。然后fieldData在每个渲染上都是对新对象的引用。

大多数时候,我依赖onChangeonBlur形成事件。

function Form() {
  const onChangeHandler = () => { /* ... */ };
  return <form onChange={onChangeHandler}>
   {/* ... */}
  </form>
}
Run Code Online (Sandbox Code Playgroud)

然后我使用useForm().getValues函数来检索当前表单值,但您需要使用模式验证来在有效时触发“自动保存”函数。

另一个解决方案(也许是一个更简单的解决方案):是使用自定义挂钩来深入比较这些值。useDeepCompareEffect您可以从react-use中查看一下。

您的代码中的 debounce 函数还有另一个错误:使用 const debouncedFunction = debounce(myFunction, 500)不起作用。“去抖”函数被记忆。由于我们处于渲染函数(功能组件)中,因此将在每次渲染时创建记忆函数,因此无论您设置的阈值如何,都会调用它。

React.useMemo为此,您需要使用:

  const { mutateAsync } = mutationResult;
  const handleDebouncedChange = React.useMemo(
    () =>
      debounce((data: NameType) => {
        mutateAsync(data);
      }, 500),
    [mutateAsync]
  );
Run Code Online (Sandbox Code Playgroud)

此处提供的完整工作版本为codesandbox :

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;
Run Code Online (Sandbox Code Playgroud)