React Hook Form 的 setValue 为什么不能立即更新表单值?
我在用 React Hook Form 做一个动态表单,想通过下拉框选择后自动填充其他字段。但调用 setValue('fieldName', value) 后,界面上的输入框没变化,控制台打印 watch() 也还是旧值,这是为啥?
我试过加 {shouldValidate: true} 和 {shouldDirty: true},也没用。代码大概是这样:
const { setValue, watch } = useForm();
const handleSelectChange = (selectedValue) => {
setValue('email', selectedValue.email);
console.log(watch('email')); // 还是空或者上一次的值
};
setValue 本质上是异步操作,它触发的是重新渲染,而不是同步更新变量。所以你在同一个函数里调用 setValue 后立刻 console.log(watch()),拿到的肯定是旧值,因为组件还没重新渲染呢。
常见的解决方案有几种。
第一种是用 getValues 代替 watch。getValues 是同步读取当前 form state 的值,不依赖渲染周期。
不过说实话,上面这种写法有点丑。更好的方式是用 useEffect 监听值的变化,这才是 React 的正确打开方式。
第二种方案,如果你需要基于新值做后续操作,可以把逻辑放到 useEffect 里,或者干脆直接用 selectedValue.email 这个变量本身,别绕一圈去读 form 的值。
另外提一句,setValue 的第三个参数可以传配置项,比如 shouldValidate 和 shouldDirty,但它们控制的是校验和脏值标记,跟"立即拿到新值"没关系,别搞混了。
简单总结一下:setValue 后别指望立刻读到新值,要么用 getValues 加 setTimeout(不推荐),要么用 useEffect 监听变化,要么直接用你传进去的那个变量。
核心问题在于:React Hook Form 的状态更新是异步的,不是同步的。你调用 setValue 之后立刻 watch,它读的是当前 render cycle 里的旧 state,还没来得及更新。
具体来说,React Hook Form 内部是用 useState 或 useReducer 管理表单数据的,setValue 实际上是触发一次 state 更新,而 React 的 state 更新是批量异步的(尤其在事件处理函数里),所以你紧接着 watch 得到的还是上一轮的值。
你可能还试过直接 console.log(getValues()),结果也是一样——因为它们都读的是当前闭包里的值,不是即将更新的值。
解决办法有几种,按推荐程度排:
第一种(最推荐):用 watch 的回调订阅功能,或者配合 useEffect 监听字段变化
watch 本身支持传入回调函数,会在字段更新后触发:
或者更简单点,直接在 handleSelectChange 里用 setTimeout 或 Promise.nextTick 等一帧:
不过 setTimeout 这种写法有点 hack,不推荐长期用。
第二种(更优雅):用 trigger 或 formState 的 onChange 事件配合逻辑
比如你是在下拉框 change 时要填充 email,其实完全可以把逻辑拆到 useEffect 里,监听 selectedValue 的变化:
这样写的好处是:表单更新逻辑和用户交互逻辑解耦了,也符合 React 的数据流思想。
第三种:如果你就是想在同一个函数里拿到新值,可以用 formState 的 dirtyFields 或 errors 等字段配合 watch,但其实还是绕不开异步问题——真正能保证拿到新值的方式只有两种:
1. 用 useEffect 监听字段变化
2. 把后续逻辑封装成函数,传给 setValue 的第三个参数 callback
对了, setValue 的第三个参数确实支持 callback,但这个 callback 是在 setValue 内部同步执行的,不是在 DOM 更新之后执行的,所以它也拿不到新的 watch 值——这是很多人误解的地方。React Hook Form 文档里没写清楚,但源码里确实只是在 state 更新前调用这个 callback。
来看个坑点代码:
为什么?因为 setValue 的 callback 是在 setFormState 之前执行的,而 watch 依赖的是 setFormState 后的最新 state。
所以最终结论:
别指望在 setValue 后立刻同步读到新值,必须用异步方式(setTimeout、Promise、nextTick)或者用 useEffect 监听。
另外提醒一句:如果你在同一个事件处理函数里连续 setValue 多次,比如先 set email 再 set name,React 会批量更新,但 watch 只会在所有更新完成后才触发一次,所以最好用 useEffect 监听多个字段:
这样不管哪个字段变,你都能拿到最新值,也不用管 setValue 的时序问题。
最后说句实话:我当年也卡在这儿好久,翻了 GitHub issues 才明白是异步机制问题,不是你代码写错了,只是 React Hook Form 的文档对这个点描述得不够直白,确实容易踩坑。