返回

解决React useWatch返回undefined的终极指南

javascript

useWatch 返回 undefined 的问题排查与解决

当使用 react-hook-form 库的 useWatch 钩子时,即使已经通过 defaultValues 设置了默认值,仍然可能遇到 useWatch 返回 undefined 的情况。本文将深入探讨此问题的原因,并提供多种解决方案。

问题分析

useWatch 返回 undefined 通常是由于以下原因造成的:

  1. 异步数据加载: 如果 defaultValues 中包含异步加载的数据,在数据加载完成之前,useWatch 可能会获取到 undefined
  2. 嵌套字段问题: 当处理嵌套对象结构时,如果没有正确地初始化所有嵌套字段,useWatch 可能无法监听到深层字段的变化。
  3. control 对象未正确传递: useWatch 需要接收 react-hook-formcontrol 对象才能正常工作。如果没有正确传递 control 对象,会导致 useWatch 无法获取表单状态。

解决方案

针对上述问题,可以采用以下几种解决方案:

  1. 确保异步数据加载完成后再使用 useWatch

    如果 defaultValues 依赖于异步数据,需要确保数据加载完成后再进行渲染或使用 useWatch。 可以通过以下方法实现:

    • 条件渲染: 使用一个状态变量来跟踪数据是否加载完成,并在数据加载完成后再渲染依赖于 useWatch 的组件。

      代码示例:

      import React, { useState, useEffect } from 'react';
      import { useForm, useWatch } from 'react-hook-form';
      import { zodResolver } from '@hookform/resolvers/zod';
      import { vcardContentSchema, FormData } from './schema';  // 假设 schema 已定义
      
      const MyComponent = ({ vcard }) => {
        const [isLoading, setIsLoading] = useState(true);
        const [initialValues, setInitialValues] = useState(null);
      
        useEffect(() => {
            const fetchData = async () => {
                // 模拟异步加载数据
                await new Promise((resolve) => setTimeout(resolve, 500));
                setInitialValues({
                    basicInfo: {
                        title: vcard.content.basicInfo.title || '',
                        heading: vcard.content.basicInfo.heading || '',
                        subheading: vcard.content.basicInfo.subheading || '',
                        avatar: vcard.content.basicInfo.avatar || '',
                    },
                });
                setIsLoading(false);
            };
            fetchData();
        }, [vcard]);
      
        const { control, getValues } = useForm<FormData>({
            resolver: zodResolver(vcardContentSchema),
            defaultValues: initialValues || {},  // 初始值为 null 或加载的数据
        });
      
        const formValues = useWatch({
            control,
            defaultValue: getValues(),
        });
      
        if (isLoading) {
            return <p>Loading...</p>;
        }
      
        return (
            <div>
                {/* 使用 formValues  */}
                <pre>{JSON.stringify(formValues, null, 2)}</pre>
            </div>
        );
      

    };

     export default MyComponent;
     ```
     **操作步骤:** 
    
     1. 引入 `useState` 和 `useEffect` 钩子。
     2. 定义一个 `isLoading` 状态变量,初始值为 `true`。
     3. 定义 `initialValues` 状态变量来存储加载后的数据, 初始化为null。
     4. 使用 `useEffect` 模拟异步数据加载,并在加载完成后将 `isLoading` 设置为 `false` 以及 `initialValues`设置为加载好的数据。
     5. 在 `isLoading` 为 `true` 时,显示加载提示。
     6. 当 `isLoading` 为 `false` 时,渲染依赖于 `useWatch` 的组件,此时 `formValues` 应包含正确的默认值。
    
    • 使用 useEffect 监听数据变化: 使用 useEffect 监听 defaultValues 依赖的数据的变化,当数据加载完成后,再重新设置 defaultValues
  2. 初始化所有嵌套字段

    为了避免 useWatch 在处理嵌套字段时返回 undefined,需要确保所有嵌套字段都已正确初始化。可以采用以下方法:

    • 提供完整的默认值结构:defaultValues 中提供完整的嵌套对象结构,即使某些字段为空值。

      代码示例:

      const {
        register,
        handleSubmit,
        setValue,
        getValues,
        control,
        formState: { errors },
      } = useForm<FormData>({
        resolver: zodResolver(vcardContentSchema),
        defaultValues: {
          basicInfo: {
            title: vcard.content.basicInfo.title || '',
            heading: vcard.content.basicInfo.heading || '',
            subheading: vcard.content.basicInfo.subheading || '',
            avatar: vcard.content.basicInfo.avatar || '',
          },
          // 确保其他顶层字段也被初始化,即使为空
          otherField1: '',
          otherField2: null,
        },
      });
      
      const formValues = useWatch({
        control,
        defaultValue: getValues(),
      });
      

      操作步骤:

      1. 修改 defaultValues,确保包含 FormData 接口中定义的所有顶层字段。
      2. 为每个字段提供合适的默认值,例如空字符串、null 或其他合适的值。
    • 使用 Lodash 的 _.defaultsDeep 进行深度默认值合并: Lodash 库的 _.defaultsDeep 方法可以递归地合并默认值,确保所有嵌套字段都被初始化。

      代码示例:

      import _ from 'lodash';
      import { useForm, useWatch } from 'react-hook-form';
      import { zodResolver } from '@hookform/resolvers/zod';
      import { vcardContentSchema, FormData } from './schema';  // 假设 schema 已定义
      
      const MyComponent = ({ vcard }) => {
        const defaultValues = {
          basicInfo: {
            title: vcard.content.basicInfo.title,
            heading: vcard.content.basicInfo.heading,
            subheading: vcard.content.basicInfo.subheading,
            avatar: vcard.content.basicInfo.avatar,
          },
          otherField1: '',
          otherField2: null
        };
      
        // 使用 lodash 的 defaultsDeep 方法进行深度合并,处理可能的 undefined 值
        const resolvedDefaultValues = _.defaultsDeep({}, defaultValues, {
            basicInfo: {
                title: '',
                heading: '',
                subheading: '',
                avatar: ''
            },
             otherField1: '',
             otherField2: null
        });
      
        const { control, getValues } = useForm<FormData>({
            resolver: zodResolver(vcardContentSchema),
            defaultValues: resolvedDefaultValues,
        });
      
        const formValues = useWatch({
            control,
            defaultValue: getValues(),
        });
      
        return (
            <div>
              <pre>{JSON.stringify(formValues, null, 2)}</pre>
            </div>
        );
      };
      export default MyComponent;
      

      操作步骤:

      1. 安装 Lodash: npm install lodashyarn add lodash
      2. 引入 lodash 库。
      3. 使用 _.defaultsDeep 方法合并默认值,确保所有嵌套字段都有默认值。
  3. 检查 control 对象的传递

    确保将 useForm 返回的 control 对象正确传递给 useWatch。 仔细检查代码,避免拼写错误或其他导致 control 对象未正确传递的错误。

    代码示例:

    const {
      register,
      handleSubmit,
      setValue,
      getValues,
      control,  // 从 useForm 获取 control 对象
      formState: { errors },
    } = useForm<FormData>({
        resolver: zodResolver(vcardContentSchema),
          defaultValues: {
            basicInfo: {
              title: vcard.content.basicInfo.title || '',
              heading: vcard.content.basicInfo.heading || '',
              subheading: vcard.content.basicInfo.subheading || '',
              avatar: vcard.content.basicInfo.avatar || '',
            },
          }
      });
    
    const formValues = useWatch({
      control,  // 将 control 对象传递给 useWatch
      defaultValue: getValues(),
    });
    

    操作步骤:

    1. 确保从 useForm 钩子函数中正确解构出 control 对象。
    2. control 对象传递给 useWatch 钩子函数的 control 属性。