React Hook Form提交时如何等待useEffect完成表单数据更新
问题核心
当前执行链路没有做统一的异步等待:点击提交时浏览器会先触发输入框的onBlur启动updateFormValues的异步请求,handleDialogSubmit不会等待这个跨事件的异步链路走完,就直接调用getValues取值。原设计靠data状态变化触发useEffect再执行reset的逻辑,本身属于渲染后的异步执行节点,无法被提交逻辑精准等待,硬编码setTimeout本质是靠运气匹配执行时间,完全无法覆盖网络延迟、React渲染节奏差异带来的时序波动。
具体改造步骤
1. 抽离useEffect中的联动逻辑,搭建可被await的统一异步链路
useEffect的定位是处理组件渲染后的副作用,不适合作为交互事件流之间的同步节点,直接把联动更新的完整逻辑从useEffect中移到updateFormValues内部,用ref存储当前正在进行的联动任务,实现跨场景的等待:
- 用
useRef存储当前正在执行的联动更新Promise,不管是onBlur触发的更新,还是提交逻辑需要等待的更新,都从这个ref中取对应任务 - 改造
updateFormValues:拿到接口返回的联动数据后,直接在当前异步流程中执行doStuff解析、formMethods.reset重置表单、更新data状态、清除loading的完整逻辑,不再依赖data变化触发useEffect才执行reset - 提交逻辑在执行校验、取值之前,先检查ref里有没有未完成的联动任务,有的话先等待任务完成再继续
改造后的核心代码示例:
// 存储当前正在进行的联动更新任务 const pendingLinkageUpdateRef = useRef<Promise<void> | null>(null); // 改造后的onBlur触发逻辑 const updateFormValues = async () => { // 避免重复发起相同的联动请求 if (pendingLinkageUpdateRef.current) { return pendingLinkageUpdateRef.current; } setLoading(true); // 把完整的联动逻辑包装为Promise存在ref中 pendingLinkageUpdateRef.current = (async () => { // 注意:联动请求要直接取react-hook-form的实时值,不要传外部的旧data状态 const result = await api.post('LINKAGE_API_ENDPOINT', formMethods.getValues()); const parsedData = doStuff(result.data); // 直接在当前流程里调用reset,不等待useEffect触发 formMethods.reset(parsedData); setData(result.data); setLoading(false); })().finally(() => { // 任务执行完成后清空ref pendingLinkageUpdateRef.current = null; }); return pendingLinkageUpdateRef.current; }; // 改造后的提交逻辑 const handleDialogSubmit = async () => { // 先等待所有未完成的联动更新走完 if (pendingLinkageUpdateRef.current) { await pendingLinkageUpdateRef.current; } const isValid = await formMethods.trigger(); if (isValid) { const allChanges = formMethods.getValues(); const result = await api.post('SUBMIT_API_ENDPOINT', allChanges); closeDialog(); } };
改造优势:完全去掉了对useEffect作为同步节点的依赖,整个联动流程和提交流程共享同一个Promise引用,不管网络返回快还是慢,提交逻辑都会等联动更新、表单reset全部走完再取值,不会出现时序错误,也不需要硬编码等待时长。
2. 修复原实现的隐性bug
原来的updateFormValues存在参数错误隐患:触发联动请求时传的是外部的data状态,这个状态本身是之前服务端联动返回的旧值,不是用户当前在输入框里刚输入、还没失焦的内容。因为react-hook-form会实时把输入框的内容同步到自身内部值中,所以联动请求的参数要直接传formMethods.getValues()的实时结果,不然就算时序问题解决了,发出去的联动请求参数本身是错的,返回的联动数据也无法正确适配用户的最新输入。
3. 现有状态逻辑兼容
原有的isLoading禁用表单字段的逻辑不需要改动:联动流程开始时就设置isLoading=true,等整个reset完成后再设置isLoading=false,完全可以避免表单刷新过程中用户重复输入的问题。
注意:改造后要删掉原来依赖data的useEffect里的reset逻辑,不然会出现两次reset的冲突问题。
内容的提问来源于stack exchange,提问作者Leviathan

