DEBUG.WIKI
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
雨云;给项目一台云服务器
晚安云;香港 CN2 ¥15/月

React Hook Form setValue 设 null 报读取 source 失败

React Hook Form 7.90.0 将默认嵌套对象改为 null 并更新 dirtyFields 时,可能递归读取空值。7.91.0 正式代码已增加空值保护,更新后同时核对当前值、脏状态及还原默认值。

React Hook Form 把字段设为 null 时,setValue() 抛出 Cannot read properties of null (reading 'source')。若调用带了 shouldDirty: true,而默认值中的同一字段是一个对象,检查是否使用了 7.90.0 的 dirtyFields 计算路径。

这里的 source 是默认对象里的普通字段名,不是必须填写的表单选项。简单的字符串默认值不一定触发;只拿字符串字段测试通过,无法排除嵌套对象递归时的空值访问。

默认对象与当前空值要一起看

下面是这一形态的最小表单配置,可在项目自己的隔离表单中核对:

import { useForm } from 'react-hook-form';

type Values = {
  value: { source: string } | null;
};

export function Example() {
  const { setValue, getValues, formState } = useForm<Values>({
    defaultValues: { value: { source: 'value' } },
  });

  return (
    <div>
      <button
        type="button"
        onClick={() => setValue('value', null, { shouldDirty: true })}
      >
        清空对象
      </button>
      <output>{JSON.stringify(formState.dirtyFields)}</output>
      <button type="button" onClick={() => console.log(getValues('value'))}>
        查看当前值
      </button>
    </div>
  );
}

例子不依赖 useFieldArray,也没有把 null 赋给不允许空值的类型。实际模型不接受 null 时,应按业务约定选择合法值;不能只为了复现异常修改生产表单的数据结构。

7.90.0 的递归比较在叶子位置直接读取 formValues[key]。默认对象仍有 source 字段、当前对象已为空时,就可能在这里读到 null 的属性。7.91.0 正式实现已在这处比较前检查 null 或 undefined,以 undefined 参与叶子比较,避免这次访问。

更新后还要核对脏状态

可在项目支持范围内采用 7.91.0 或已确认保留该处理的后续正式版本,更新锁文件并确认应用实际加载的 react-hook-form。不要根据堆栈中的打包行号推断版本;开发服务器缓存、工作区链接和多版本依赖可能让运行代码与 package.json 不一致。

不要用 shouldDirty: false 作为等价修复。它跳过了需要的脏状态更新,可能让保存按钮、离开页面提示和依赖 dirtyFields 的逻辑失效。把默认对象改成 null 也会改变“与什么比较”的基准,只有默认状态确实应为空时才合适。

这次操作应保持当前 value 为 null,并准确表示它与原默认对象不同。再把值恢复为相同结构和内容,确认脏状态按项目约定恢复;同时核对提交数据仍为空值,而不是为绕过递归临时替换成 {}、空字符串或旧对象。

若字段由 Controller、字段数组或外部 values 控制,继续检查对应更新关系,不把这次空值保护概括成所有表单状态问题的修复。异步提交也不应因为第一次 setValue 抛错就自动重复,先确认业务动作是否已经发生。