如何实现React Final Form请求后设值、校验并避免循环请求
React Final Form 动态配置表单问题解决方案
核心思路
所有问题的根源是两个逻辑边界没有划清:
- 把后端动态更新的校验规则当成了表单初始化时的静态配置,依赖RFF默认的事件触发校验,没有做显式的校验触发
- 没有区分「用户主动输入触发的值变更」和「接口返回同步的被动值变更」,导致自动保存逻辑误判
问题1:动态校验规则更新后不触发即时校验的修复
- 把后端返回的校验规则存在独立的页面state中,表单全局validate函数始终读取最新的state值,不要依赖组件初始化时闭包缓存的旧规则。
- 废弃之前通过
<Form initialValues={fn} />传函数同步表单值的写法:RFF的initialValues仅在表单首次挂载、调用form.reset()时生效,动态传入不会触发值同步,反而会导致状态不一致。动态更新表单值统一用RFF表单实例的form.change()方法,批量更新时包裹在form.batch()中减少不必要的重渲染。 - 每次接口返回更新校验规则、同步完字段值之后,主动调用表单实例的
form.validate()方法强制全表单重跑校验,校验结果会即时同步到字段错误提示、提交按钮disabled状态,不需要等用户触发change/blur事件。
问题2:接口返回值更新触发冗余请求/请求循环的修复
- 新增一个ref标记位
isSyncingFromApi,默认值为false,专门标识当前的值变更是来自接口同步,而非用户主动操作。ref不会触发重渲染,也不存在闭包旧值问题。 - 每次拿到接口返回、准备同步更新表单值和校验规则之前,先把
isSyncingFromApi.current设为true,等所有更新完成、AutoSave组件的值变更检测逻辑执行完毕后,再在下一个事件循环把标记位改回false。 - 修改AutoSave组件的触发条件:仅当表单值变更、校验通过、*且
isSyncingFromApi.current为false*时,才触发防抖自动请求。接口同步触发的值变更直接跳过请求逻辑,从根源上避免循环调用。 - 增加一层兜底校验:每次触发自动请求前,把当前表单值和上一次成功发送请求的值做深比较,如果值完全一致就直接终止请求,就算标记位因为时序问题出现漏判,也不会发送重复请求。
核心代码参考
import { useRef, useState } from 'react' import { Form, Field } from 'react-final-form' // 深比较、深拷贝可直接用lodash对应方法,此处不额外实现 import { isEqual as deepEqual, cloneDeep } from 'lodash' const DynamicForm = () => { // 存储后端返回的最新校验配置 const [validationConfig, setValidationConfig] = useState({}) // 接口同步状态标记 const isSyncingFromApi = useRef(false) // 缓存上一次成功提交的表单值,做重复请求兜底 const lastSubmittedValues = useRef({}) // 存储RFF表单实例引用 const formRef = useRef(null) // 全局校验函数,始终读取最新的validationConfig const validateForm = (formValues) => { const errors = {} Object.entries(formValues).forEach(([fieldKey, fieldVal]) => { const currentRule = validationConfig[fieldKey] if (!currentRule) return // 按照后端返回的规则做实际校验,以下为示例逻辑 if (Number(fieldVal.value) < Number(currentRule.validation_value)) { errors[fieldKey] = currentRule.validation_msg } }) return errors } // 自动请求逻辑 const triggerAutoRequest = async (formValues) => { // 接口同步中,直接跳过 if (isSyncingFromApi.current) return // 和上次提交的值完全一致,跳过重复请求 if (deepEqual(formValues, lastSubmittedValues.current)) return // 发送接口请求 const res = await fetch('/api/form/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formValues) }).then(res => res.json()) // 开启同步标记 isSyncingFromApi.current = true // 更新校验配置 setValidationConfig({ field1: res.field1, field2: res.field2 }) // 批量更新表单值,减少重渲染 formRef.current.batch(() => { formRef.current.change('field1', res.field1) formRef.current.change('field2', res.field2) }) // 强制全表单校验 formRef.current.validate() // 缓存本次提交的值 lastSubmittedValues.current = cloneDeep(formValues) // 等同步流程走完后关闭标记 setTimeout(() => { isSyncingFromApi.current = false }, 0) } return ( <Form onSubmit={() => {}} // 自动保存场景无需手动提交逻辑 validate={validateForm} initialValues={{ field1: { value: '500', dropdown_value: 'A' }, field2: { value: '1000', dropdown_value: 'B' } }} render={({ handleSubmit, form, invalid }) => { formRef.current = form return ( <form onSubmit={handleSubmit}> {/* 此处渲染Field字段、传入triggerAutoRequest给AutoSave组件即可 */} <button type="submit" disabled={invalid}>提交</button> </form> ) }} /> ) } export default DynamicForm
注意事项
- 批量更新表单值一定要用
form.batch()包裹,否则每个字段的change调用都会触发一次全表单重渲染和校验,会有明显性能问题。 - 不要用state存同步标记,state变更会触发额外重渲染,ref是更合适的选择。
- 如果有字段级的异步校验,需要等
form.validate()执行完返回Promise状态后再更新UI,避免出现校验状态闪烁。
内容的提问来源于stack exchange,提问作者sbstnssndn
相关产品推荐
相关产品推荐

