React 复杂表单状态机:用 useReducer 管理多步向导与联动校验

test12026-09-241 次阅读

为什么多步表单不适合纯 useState

一个三步注册向导(账号、资料、确认)如果拆成十几个 useState,步骤切换、字段联动、整页重置都散落在各处,组件很快变成一团难以追踪的状态。当某个字段变化需要同时影响多个派生值时,纯 useState 会出现"先 setA 再 setB"的时序问题。useReducer 把"当前状态 + 动作"收敛成纯函数,所有变更都走同一入口,便于推理和测试。

定义状态与动作

先用类型约束住 state 与 action,再用一个 reducer 描述每种动作如何产生新 state。下面是一段可直接放进项目的实现。

type FormState = {
  step: number;
  account: { email: string; password: string };
  profile: { nickname: string; city: string };
  errors: Record<string, string>;
};

type Action =
  | { type: "SET_FIELD"; section: "account" | "profile"; key: string; value: string }
  | { type: "NEXT" }
  | { type: "PREV" }
  | { type: "SET_ERRORS"; errors: Record<string, string> }
  | { type: "RESET" };

const initial: FormState = {
  step: 0,
  account: { email: "", password: "" },
  profile: { nickname: "", city: "" },
  errors: {},
};

function reducer(state: FormState, action: Action): FormState {
  switch (action.type) {
    case "SET_FIELD":
      return {
        ...state,
        [action.section]: { ...state[action.section], [action.key]: action.value },
        errors: { ...state.errors, [action.key]: "" },
      };
    case "NEXT":
      return { ...state, step: Math.min(state.step + 1, 2) };
    case "PREV":
      return { ...state, step: Math.max(state.step - 1, 0) };
    case "SET_ERRORS":
      return { ...state, errors: action.errors };
    case "RESET":
      return initial;
    default:
      return state;
  }
}

在组件里驱动

组件只负责派发动作,校验逻辑也可放进一个纯函数,校验失败时把错误写回 reducer,而不是各自维护零散的 error state。

import { useReducer } from "react";

function validateAccount(s: FormState): Record<string, string> {
  const e: Record<string, string> = {};
  if (!/^\S+@\S+$/.test(s.account.email)) e.email = "邮箱格式不正确";
  if (s.account.password.length < 8) e.password = "密码至少 8 位";
  return e;
}

export function Wizard() {
  const [state, dispatch] = useReducer(reducer, initial);

  const goNext = () => {
    if (state.step === 0) {
      const e = validateAccount(state);
      if (Object.keys(e).length) return dispatch({ type: "SET_ERRORS", errors: e });
    }
    dispatch({ type: "NEXT" });
  };

  return (
    <div>
      <p>当前步骤:{state.step + 1}/3</p>
      {state.errors.email && <p>{state.errors.email}</p>}
      <button onClick={() => dispatch({ type: "PREV" })}>上一步</button>
      <button onClick={goNext}>下一步</button>
      <button onClick={() => dispatch({ type: "RESET" })}>重置</button>
    </div>
  );
}

什么时候该用

  • 字段多、有步骤或明显状态流转(草稿、提交中、成功、失败)时,useReducer 让转移规则集中可控。
  • 多个字段互相联动、或一处操作要改多处派生值时,纯函数 reducer 避免了 "set 顺序" 陷阱。
  • 只是单个输入框,useState 更直观;不要为了用而用。
T

test1

文章作者

为什么多步表单不适合纯 useState 一个三步注册向导(账号、资料、确认)如果拆成十几个 useState,步骤切...

分类
技术
发布时间
2026-09-24
字数
约 2641 字
阅读
1 次