用好 Final Form 实现高效表单状态管理
我的写法,亲测靠谱
我用 Final Form 已经三年多了,从 React 16 那会儿就开始用,一直到现在 React 18 也没换过表单库。不是说它完美,而是它够轻、够灵活、类型支持也还行。但坑是真不少,尤其是刚上手那阵子,各种状态同步问题把我整得怀疑人生。
先说最核心的:我一般这样组织 form 的结构。
import { Form, Field } from 'react-final-form';
import arrayMutators from 'final-form-arrays';
import { FieldArray } from 'react-final-form-arrays';
const MyForm = () => {
const onSubmit = async (values) => {
try {
const res = await fetch('https://jztheme.com/api/submit', {
method: 'POST',
body: JSON.stringify(values),
headers: { 'Content-Type': 'application/json' }
});
if (!res.ok) throw new Error('提交失败');
alert('提交成功!');
} catch (err) {
return { [FORM_ERROR]: err.message };
}
};
return (
<Form
onSubmit={onSubmit}
mutators={{ ...arrayMutators }}
initialValues={{ users: [{ name: '', email: '' }] }}
render={({ handleSubmit, submitting, submitError }) => (
<form onSubmit={handleSubmit}>
<FieldArray name="users">
{({ fields }) =>
fields.map((name, index) => (
<div key={name} style={{ marginBottom: '1rem' }}>
<div>
<label>姓名</label>
<Field name={${name}.name} component="input" placeholder="输入姓名" />
</div>
<div>
<label>邮箱</label>
<Field name={${name}.email} component="input" placeholder="输入邮箱" />
</div>
<button type="button" onClick={() => fields.remove(index)}>
删除
</button>
</div>
))
}
</FieldArray>
<button type="button" onClick={() => fields.push('users', undefined)}>
添加用户
</button>
{submitError && <div style={{ color: 'red' }}>{submitError}</div>}
<button type="submit" disabled={submitting}>
{submitting ? '提交中...' : '提交'}
</button>
</form>
)}
/>
);
};
这段代码有几个关键点:
- mutators 必须传:如果你用到了数组操作(比如动态增减字段),不传
arrayMutators会导致push、remove这些方法无效。我踩过这个坑,调试半天发现文档里藏了一句“记得引入 mutators”。 - 错误处理统一在
onSubmit返回对象,key 是FORM_ERROR,这样外面可以通过meta.submitError拿到。 FieldArray要配合Field使用,不要自己瞎 setState 去改 values,不然校验和 touched 状态就乱了。
这种写法的好处是:逻辑集中、状态可控、容易加校验、支持动态字段。而且 render props 虽然看起来啰嗦,但比 HOC 清晰多了——至少我知道每个组件拿到的是啥。
这几种错误写法,别再踩坑了
见过太多人这么写:
// ❌ 错误示范1:直接修改 formState.values
const handleChange = (e, fieldName) => {
form.mutators.setValue(fieldName, e.target.value);
};
或者更离谱的:
// ❌ 错误示范2:绕开 Final Form 自己搞 state
const [formData, setFormData] = useState(initialValues);
<Field name="username" onChange={(v) => setFormData({...formData, username: v})} />
兄弟,你都用 Final Form 了,为啥还要自己管 state?这不是双重来源吗?结果就是:UI 显示的值和 form 内部的值对不上,touched、dirty 这些 flag 全都失效。我之前接手一个项目就这么干的,修了两天才理清楚。
还有人喜欢在 onSubmit 里面手动调接口,然后用 try/catch 包一层,但忘了返回 error 对象:
// ❌ 错误示范3:没返回错误,用户看不到反馈
const onSubmit = async (values) => {
try {
await api.submit(values);
} catch (err) {
console.error(err); // 只打日志,不返回
}
};
这会导致界面上一点反应都没有,用户点了提交按钮像石沉大海。正确的做法是返回一个对象,Final Form 会自动挂到 formState.submitError 上。
还有一个常见误区:滥用 subscription。
// ❌ 订阅整个 formState,性能杀手
<Form
subscription={{ values: true, touched: true, errors: true, dirty: true }}
render={({ values }) => <pre>{JSON.stringify(values)}</pre>}
/>
你以为你在展示数据,实际上每次字段变化都会导致整个组件 rerender。尤其是复杂表单,页面卡得像老牛拉车。建议只订阅你需要的部分:
// ✅ 只订阅 submitting 和 errors
<Form
subscription={{ submitting: true, submitError: true }}
render={({ handleSubmit, submitting, submitError }) => (
// ...
)}
/>
实际项目中的坑
第一个大坑:**异步校验怎么搞?**
很多人图省事,在 validate 函数里直接 await checkEmailExists(value)。错!Final Form 的 validate 是同步的,你扔个 promise 进去,它不会等,直接当成没有错误处理了。
正确姿势是用 asyncValidate,配合 asyncBlurFields:
<Form
asyncValidate={async (values) => {
if (!values.email) return;
const exists = await fetch(https://jztheme.com/api/check-email?email=${values.email});
if (exists) throw { email: '邮箱已存在' };
}}
asyncBlurFields={['email']}
// ...
/>
注意:asyncBlurFields 指定哪些字段触发异步校验(一般是失焦时)。别全开,不然输个字就发请求,后端要炸。
第二个坑:**初始值更新无效。**
你以为 initialValues 支持受控?天真了。Final Form 默认只在 mount 时读一次 initialValues,之后你父组件 setState 改了传进去的值,form 里面还是老样子。
解决办法有两个:
- 用
form.reset()手动重置(适合弹窗表单) - 加
enableReinitialize: true—— 但这玩意官方已经标记为 deprecated,说未来会删
所以我现在的做法是:如果数据来自接口加载,我就等数据回来再渲染 <Form>,而不是一开始就塞个空对象进去。
{data ? <Form initialValues={data} /> : <div>加载中...</div>}
第三个实战细节:**自定义组件怎么接 Field?**
别直接把 input 属性透传给第三方组件。比如你用了 Ant Design 的 Input:
// ❌ 错误写法
<Field name="username" children={({ input }) => <Input {...input} />} />
这样会导致 onChange 接收的是 event,但 Antd Input 的 onChange 返回的是 value,不是 event。于是 input.value === undefined,值就丢了。
正确写法:
<Field name="username">
{({ input: { value, onChange, ...inputProps } }) => (
<Input
{...inputProps}
value={value}
onChange={(val) => onChange(val)} // 注意这里
/>
)}
</Field>
或者封装成通用适配器函数,避免重复劳动。
最后的小技巧
1. 表单多了之后,我把通用逻辑抽成 hook:
const useFormSubmit = (apiUrl) => (values) =>
fetch(apiUrl, {
method: 'POST',
body: JSON.stringify(values)
}).then(res => {
if (!res.ok) throw new Error('提交失败');
});
然后在 Form 里直接用 onSubmit={useFormSubmit('https://jztheme.com/api/user')} />,省事。
2. 开发阶段可以加个 debug 工具:
<pre>{JSON.stringify(form.getState(), null, 2)}</pre>
贴在表单下面,实时看状态变化,排查问题快得多。
3. 不要用 Final Form 做超复杂表单(比如上百字段带联动的那种)。真有这种需求,考虑拆成多个子 form,或者上 Redux 表单方案。Final Form 适合中小型、结构清晰的场景。
总结一下
以上是我用 Final Form 这几年攒下来的经验。说实话这库学习曲线有点陡,API 设计也不够直观,但一旦掌握,写起表单来还是很顺手的。关键是别乱搞 state,别绕开它的机制,老老实实用它的模式。
现在虽然 React Hook Form 很火,但我还没切换。不是因为它不好,而是现有项目稳定运行,没必要折腾。不过新项目如果要求高性能、TS 友好,我会优先考虑 Hook Form。
以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流。

暂无评论