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 抛错就自动重复,先确认业务动作是否已经发生。


