React 表单状态管理经验总结:复杂表单不该一路 useState 写到底

0 阅读

React 里最容易写、也最容易写乱的东西之一,就是表单。

刚开始做简单输入框时,useState 足够直接。但一旦表单里出现下面这些需求,复杂度会迅速上升:

  • 动态字段
  • 联动校验
  • 草稿保存
  • 分步提交
  • 错误提示
  • 异步校验

这时如果还坚持“一个字段一个 useState”,代码很快就会失控。

最先出问题的通常不是校验,而是状态结构

很多表单越来越难维护,并不是因为规则复杂,而是因为状态没有分层。

一个更清晰的做法通常是把表单拆成三类状态:

  1. 表单值
  2. 校验状态
  3. 提交流程状态

例如:

1const [values, setValues] = useState({
2  name: '',
3  email: '',
4  company: ''
5});
6
7const [errors, setErrors] = useState({});
8const [isSubmitting, setIsSubmitting] = useState(false);

如果把这些状态混在一起,后面几乎必然出现“改一个字段,顺手影响整个提交流程”的耦合问题。

表单值适合集中,交互状态适合分开

values 可以集中管理,因为它们本质上属于一份数据快照。

但像下面这些状态,不建议都塞进一个大对象里:

  • 当前是否提交中
  • 哪个字段正在异步校验
  • 是否显示确认弹窗
  • 当前步骤是否可前进

这些属于流程或 UI 状态,和表单值不是一个层次。

复杂表单更适合 reducer

当字段很多、更新动作明确时,useReducer 往往比一堆 setState 更稳。

1function formReducer(state, action) {
2  switch (action.type) {
3    case 'update_field':
4      return {
5        ...state,
6        values: {
7          ...state.values,
8          [action.name]: action.value
9        }
10      };
11    case 'set_errors':
12      return {
13        ...state,
14        errors: action.errors
15      };
16    case 'set_submitting':
17      return {
18        ...state,
19        isSubmitting: action.value
20      };
21    default:
22      return state;
23  }
24}

reducer 的价值不只是“统一写法”,而是把状态变化变成显式动作,后面调试和扩展都会更容易。

校验不要写成“哪里触发就在哪里拼”

表单校验最容易烂掉的方式,是在 onChangeonBluronSubmit 里各写一份条件分支。

更靠谱的方式是:

  • 把校验逻辑提成单独函数
  • 明确区分字段级校验和表单级校验
  • 不同触发时机复用同一套规则

例如:

1function validate(values) {
2  const errors = {};
3
4  if (!values.name.trim()) {
5    errors.name = '姓名不能为空';
6  }
7
8  if (!values.email.includes('@')) {
9    errors.email = '邮箱格式不正确';
10  }
11
12  return errors;
13}

这样至少能保证规则只有一个来源,而不是散落在多个事件里各写一份。

异步校验一定要考虑竞态

很多表单会在输入用户名、邮箱、邀请码时做异步校验。

这类场景最容易出现的问题是:

  • 用户输入 A,发请求
  • 很快又输入 B,再发请求
  • A 的结果后回来,把 B 的结果覆盖了

所以异步校验至少要考虑:

  • 请求取消
  • 只处理最新一次响应
  • 加上最小防抖

否则你看到的并不是“校验结果”,而是“返回顺序”。

提交流程不要只维护一个 loading

很多表单最后只有一个 loading 状态,这在简单场景没问题,但复杂一点就不够了。

例如提交过程里可能有:

  1. 本地校验
  2. 图片上传
  3. 主表提交
  4. 成功回跳

这些阶段都叫 loading,等于没有信息。

更实际的做法是用阶段状态来表达:

  • idle
  • validating
  • uploading
  • submitting
  • success
  • error

一旦流程出问题,你会更容易定位卡在哪一步。

什么时候该用表单库

如果你的表单有这些特征:

  • 字段数量多
  • 规则复杂
  • 需要和 UI 组件库深度整合
  • 多个页面都有类似模式

那引入表单库通常是合理的。

但核心依然不是“为了用库而用库”,而是你是否已经清楚:

  • 状态怎么分层
  • 校验怎么组织
  • 提交流程怎么表达

如果这些都没想清楚,换什么库也只是在换一种混乱方式。

写在最后

复杂表单的难点从来不只是输入和提交,而是状态、规则和流程三者的组织方式。

在 React 里,表单写得稳不稳,关键不在于用了多少 Hook,而在于你有没有把“数据”“校验”“流程”分开建模。只要这一步做对,后面的代码会清爽很多。