React 用 useReducer 管理复杂表单:多字段联动、校验与一键重置

发布时间:2026/8/9 12:11:50
React 用 useReducer 管理复杂表单:多字段联动、校验与一键重置
React 用 useReducer 管理复杂表单:多字段联动、校验与一键重置一个注册表单,五六个字段,你用useState一个字段一个状态,写着写着变量堆成一片:name/setName、email/setEmail、password/setPassword……再加上每个字段的错误信息、是否 touched、提交 loading,十几个useState挤在组件顶部。改个校验逻辑要动好几处,重置表单还得一个个setXxx()手动清。这种「一坨相关状态 有联动逻辑」的场景,正是useReducer的主场。这篇带你把一个多字段表单从 useState 泥潭重构成 reducer,顺带把校验和重置一并收拾干净。先看 useState 版本的痛点function SignupForm() { const [name, setName] useState(); const [email, setEmail] useState(); const [password, setPassword] useState(); const [errors, setErrors] useState({}); const [submitting, setSubmitting] useState(false); // ...改一个字段要 onChange 里 setXxx,校验要 setErrors,重置要一堆 setXxx() }问题不在于「能不能写」,而在于状态是散的:字段值、错误、提交态明明是同一份表单状态,却被拆成互不相干的变量,任何跨字段的逻辑(比如「改了密码就清掉密码的错误」)都要跨多个 setter 协调,很容易漏。用 useReducer 把表单状态收成一个对象reducer 的核心思想:所有表单状态放一个对象,所有变更走一个dispatch(action),变更逻辑集中在 reducer 里。import { useReducer } from react; const initialState { values: { name: , email: , password: }, errors: {}, submitting: false, }; function reducer(state, action) { switch (action.type) { case CHANGE: return { ...state, values: { ...state.values, [action.field]: action.value }, // 用户一改这个字段,就顺手清掉它旧的错误提示(联动逻辑集中在这) errors: { ...state.errors, [action.field]: undefined }, }; case SET_ERRORS: return { ...state, errors: action.errors }; case SUBMIT_START: return { ...state, submitting: true }; case SUBMIT_END: return { ...state, submitting: false }; case RESET: return initialState; // 一键重置:直接返回初始对象 default: return state; } }注意RESET的优雅:不用一个个清字段,return initialState一行搞定,这正是把状态收成一个对象的红利。组件里:一个 onChange 通吃所有字段function SignupForm() { const [state, dispatch] useReducer(reducer, initialState); const { values, errors, submitting } state; // 一个 handler 处理所有输入框,靠 name 属性区分字段 const handleChange (e) { dispatch({ type: CHANGE, field: e.target.name, value: e.target.value }); }; return ( form onSubmit{handleSubmit} input namename value{values.name} onChange{handleChange} / {errors.name span classNameerr{errors.name}/span} input nameemail value{values.email} onChange{handleChange} / {errors.email span classNameerr{errors.email}/span} input namepassword typepassword value{values.password} onChange{handleChange} / {errors.password span classNameerr{errors.password}/span} button typesubmit disabled{submitting} {submitting ? 提交中... : 注册} /button button typebutton onClick{() dispatch({ type: RESET })} 重置 /button /form ); }十几个 useState 十几个 setter,压缩成values.xxx 一个handleChange。加字段?改initialState加一行、JSX 加一个 input,reducer 一个字不用动。校验:把纯逻辑抽成独立函数,提交时统一跑校验不该塞进 reducer(reducer 要保持纯、可预测),抽成普通函数:function validate(values) { const errors {}; if (!values.name.trim()) errors.name 请填写用户名; if (!/^[^][^]\.[^]$/.test(values.email)) errors.email 邮箱格式不对; if (values.password.length 6) errors.password 密码至少 6 位; return errors; } async function handleSubmit(e) { e.preventDefault(); const errs validate(values); if (Object.keys(errs).length 0) { dispatch({ type: SET_ERRORS, errors: errs }); // 有错就回显,不提交 return; } dispatch({ type: SUBMIT_START }); try { await api.signup(values); dispatch({ type: RESET }); // 成功后一键清空 } finally { dispatch({ type: SUBMIT_END }); // 无论成败都关掉 loading } }handleSubmit要写在组件内部(它用到了values和dispatch)。这里的分工很清晰:reducer 管「状态怎么变」,validate 管「数据对不对」,handler 管「什么时候触发」,三者解耦,改校验规则只动validate。进阶:提交时把状态和 dispatch 用 useCallback 稳定引用如果表单要拆成子组件,把dispatch传下去时不用useCallback包——dispatch的引用 React 保证稳定,不会每次渲染都变,子组件用React.memo也不会因它而重渲染。这是 reducer 相比「传一堆 setter」的又一个好处:// 子组件:dispatch 引用稳定,配合 memo 不会无谓重渲染 const Field React.memo(function Field({ name, value, error, dispatch }) { return ( div input name{name} value{value} onChange{(e) dispatch({ type: CHANGE, field: name, value: e.target.value })} / {error span classNameerr{error}/span} /div ); });传多个 setter 下去时,每个 setter 引用虽然也稳定,但数量一多就啰嗦;一个dispatch走天下更干净。什么时候别用 useReducer不是所有表单都值得上 reducer。一两个独立字段、没有联动、没有复杂校验,useState更直接,别为了「显得高级」硬套。useReducer 的性价比在于:字段多(4)、字段间有联动、有统一的校验/提交/重置流程——状态越复杂、变更逻辑越集中,它越划算。小结多字段、有联动、有统一校验/重置的表单,把状态收成一个对象交给useReducer,比一堆useState清爽得多。所有变更走dispatch(action),逻辑集中在 reducer;联动(如「改字段清该字段错误」)写在对应 case 里。一键重置只需 reducer 里return initialState,不用逐个 setXxx。校验抽成纯函数validate(values),提交时统一跑,和 reducer、handler 三者解耦。dispatch引用天然稳定,传给memo子组件不会引发无谓重渲染,比传一堆 setter 更省心。一句话记忆点:字段一多就把散装 useState 收成一个 reducer——状态一个对象、变更一个 dispatch、重置一行搞定。