用好 Final Form 实现高效表单状态管理

W″景叶 交互 阅读 1,857
赞 10 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

我用 Final Form 已经三年多了,从 React 16 那会儿就开始用,一直到现在 React 18 也没换过表单库。不是说它完美,而是它够轻、够灵活、类型支持也还行。但坑是真不少,尤其是刚上手那阵子,各种状态同步问题把我整得怀疑人生。

用好 Final Form 实现高效表单状态管理

先说最核心的:我一般这样组织 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 会导致 pushremove 这些方法无效。我踩过这个坑,调试半天发现文档里藏了一句“记得引入 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 里面还是老样子。

解决办法有两个:

  1. form.reset() 手动重置(适合弹窗表单)
  2. 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。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论