Formik 异步表单提交实战:用 async/await 优雅处理 onSubmit 与 isSubmitting
【免费下载链接】formikBuild forms in React, without the tears 😭项目地址: https://gitcode.com/gh_mirrors/fo/formik
导读
在 React 表单开发中,提交数据到后端通常涉及网络请求、等待响应、处理错误等异步操作。本篇文章基于 Formik 官方仓库中的 async-submission 示例,完整讲解如何用async/await编写 Formik 的onSubmit处理器,并深入剖析isSubmitting、setSubmitting、submitForm等底层机制——让你不仅能照抄示例,还能理解 Formik 提交周期的完整内部原理,写出防重复提交、状态可控的健壮表单。
一、示例概述:一份可直接运行的异步提交表单
Formik 仓库的examples/async-submission目录是一个最小可运行的 Create React App 示例,目录结构如下:
- examples/async-submission/README.md:示例说明文档,明确指出"该示例演示了如何使用 async/await 提交 Formik 表单";
- examples/async-submission/index.js:示例的完整源码入口;
- examples/async-submission/package.json:示例的依赖与运行配置。
其中 package.json 声明的依赖为:react@^16.12.0、react-dom@^16.12.0、react-scripts@3.4.1以及formik: "latest",即这是一个基于 Create React App 的标准 React 应用。你可以将该目录单独复制出来,执行npm install && npm start运行,也可以在浏览器中直接打开index.html对应的构建产物观察效果(示例为纯前端演示,无需后端服务)。
二、核心代码拆解:async onSubmit 的最小范式
示例的核心逻辑非常精简,全部位于 index.js 中,它演示了 Formik 异步提交的完整范式,可以概括为以下三个关键部分。
1. 用 sleep 模拟真实网络请求
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));这是一个典型的"模拟异步任务"工具函数:它返回一个在指定毫秒数后 resolve 的 Promise。在真实项目中,这里通常是fetch('/api/signup', { method: 'POST', body: JSON.stringify(values) })或 axios 请求——二者本质上都是返回 Promise 的异步操作,因此下面的写法完全通用。
2. onSubmit 定义为 async 函数
onSubmit={async (values) => { await sleep(500); alert(JSON.stringify(values, null, 2)); }}这里有两个关键点:
onSubmit被声明为async函数,因此它总是返回一个 Promise;- 函数体内通过
await sleep(500)模拟 500ms 的提交耗时,随后将表单的values以 JSON 字符串形式弹出展示。
由于async函数天然返回 Promise,Formik 内部就能感知"提交尚未完成",从而维持isSubmitting为true,直到 Promise resolve 后自动将其重置为false。这正是本示例要演示的核心行为:提交期间表单按钮保持禁用状态,提交完成后自动恢复。
3. 用 isSubmitting 禁用提交按钮,防止重复提交
{({ isSubmitting }) => ( <Form> <label htmlFor="firstName">First Name</label> <Field name="firstName" placeholder="Jane" /> <label htmlFor="lastName">Last Name</label> <Field name="lastName" placeholder="Doe" /> <label htmlFor="email">Email</label> <Field name="email" placeholder="jane@acme.com" type="email" /> <button type="submit" disabled={isSubmitting}> Submit </button> </Form> )}通过 Formik 的 render-prop 模式,从上下文中解构出isSubmitting,将其绑定到提交按钮的disabled属性上:
- 用户点击提交后,
isSubmitting变为true,按钮立刻变灰,从 UI 层面阻止用户在请求进行期间再次点击; - 异步提交完成(Promise resolve)后,Formik 自动把
isSubmitting置回false,按钮恢复可点击。
这是 Formik 官方文档中推荐的"防止双重提交"标准做法(详见 表单提交流程指南 中 FAQ:"Disable whatever is triggering submission ifisSubmittingistrue")。
4. 表单 UI 的组织方式
示例中使用了 Formik 的三件套组件:
<Formik>:顶层容器,接收initialValues与onSubmit;<Form>:渲染原生<form>元素并自动接管onSubmit/onReset(见 Form.tsx 的实现,它通过useFormikContext()取出handleSubmit与handleReset绑定到<form>标签上);<Field>:将输入控件与 Formik 状态绑定(见 Field.tsx,其底层调用getFieldProps注入value、onChange、onBlur到控件上)。
注意initialValues中声明了firstName、lastName、email三个字段,onSubmit收到的values参数正是这三个字段的当前值对象。
三、深入源码:Formik 异步提交的完整生命周期
理解了示例代码后,我们进入 packages/formik/src/Formik.tsx 的源码,看看点击提交按钮后,Formik 内部究竟发生了什么。整个提交流程可以拆解为"预提交 → 校验 → 执行提交"三个阶段,这一阶段划分在官方 表单提交流程指南 中有明确说明。
阶段一:预提交(Pre-submit)
当用户点击type="submit"的按钮或调用submitForm()时,submitForm(Formik.tsx 第 742 行)会向 reducer 派发SUBMIT_ATTEMPT消息:
dispatch({ type: 'SUBMIT_ATTEMPT' });对应 reducer(Formik.tsx 第 97 行)的处理逻辑为:
case 'SUBMIT_ATTEMPT': return { ...state, touched: setNestedObjectValues<FormikTouched<Values>>(state.values, true), isSubmitting: true, submitCount: state.submitCount + 1, };即一次完成三件事:
- touch 所有字段(
setNestedObjectValues(state.values, true))——Formik 会在提交前将所有字段标记为"已访问",这样所有隐藏的错误提示都会显示出来; - 将
isSubmitting置为true; submitCount加一,用于记录提交次数。
阶段二:校验(Validation)
submitForm随后调用validateFormWithHighPriority(),它会并行执行字段级校验、validate函数与validationSchema(Yup 等)并深度合并结果:
return validateFormWithHighPriority().then((combinedErrors) => { const isInstanceOfError = combinedErrors instanceof Error; const isActuallyValid = !isInstanceOfError && Object.keys(combinedErrors).length === 0; if (isActuallyValid) { // 进入执行提交阶段 } else { dispatch({ type: 'SUBMIT_FAILURE' }); ... } });- 若存在校验错误:派发
SUBMIT_FAILURE,将isSubmitting重置为false(reducer 见 Formik.tsx 第 107 行),提交被中止; - 若校验通过:进入阶段三。
阶段三:执行提交(Submission)
校验通过后,submitForm会调用executeSubmit(Formik.tsx 第 861 行):
const executeSubmit = useEventCallback(() => { return onSubmit(state.values, imperativeMethods); });executeSubmit的本质就是调用你传入的onSubmit(values, formikBag)。接下来源码中的关键逻辑是判断onSubmit返回的是不是 Promise(相关注释见 Formik.tsx 第 759-766 行):
let promiseOrUndefined; try { promiseOrUndefined = executeSubmit(); // 同步函数返回 undefined,交由开发者自行调用 setSubmitting(false) if (promiseOrUndefined === undefined) { return; } } catch (error) { throw error; } return Promise.resolve(promiseOrUndefined) .then(result => { if (!!isMounted.current) { dispatch({ type: 'SUBMIT_SUCCESS' }); } return result; }) .catch(_errors => { if (!!isMounted.current) { dispatch({ type: 'SUBMIT_FAILURE' }); throw _errors; } });这段代码揭示了 Formik 异步提交的两个关键设计:
- async onSubmit 自动收尾:当
onSubmit是 async 函数(返回 Promise)时,Formik 会在 Promise resolve 后自动派发SUBMIT_SUCCESS,把isSubmitting重置为false——因此示例中不需要手动调用setSubmitting(false); - 同步 onSubmit 需要手动收尾:如果
onSubmit是同步函数(返回undefined),Formik 会直接跳出,此时必须由开发者在函数末尾手动调用formikBag.setSubmitting(false)来结束提交周期,否则isSubmitting将永远停留在true。
此外,Promise 被 reject(提交失败)时会派发SUBMIT_FAILURE,同样将isSubmitting重置为false,但会继续向上抛出错误。
isPromise 判定工具
Formik 内部通过 utils.ts 中的isPromise工具函数(第 36 行)来判断提交函数是否异步:
/** @private is the given object/value a promise? */ export const isPromise = (value: any): value is PromiseLike<any> => isObject(value) && isFunction(value.then);即"是对象且具有可调用的then方法"即视为 PromiseLike。async函数返回的对象恰好满足这一特征,所以 Formik 能准确识别异步提交。
handleSubmit 与
当你在 JSX 中使用<Form>组件时,它内部会自动把handleSubmit绑定到原生<form>的onSubmit上(见 Form.tsx)。handleSubmit(Formik.tsx 第 807 行)会依次执行:
e.preventDefault()阻止浏览器默认提交行为(整页刷新);e.stopPropagation()阻止事件冒泡;- 调用
submitForm()进入上面描述的完整提交流程。
开发环境下它还会检查触发提交的按钮是否缺少type属性,并给出警告提示,以避免 reset 按钮误触发提交的常见坑。
四、onSubmit 的签名与同步/异步对照
根据 API 文档 对onSubmit的定义(formik.md 第 388 行):
onSubmit: (values: Values, formikBag: FormikBag) => void | Promise<any>values:当前表单的字段值对象,与initialValues结构一致;formikBag:一组命令式方法,包括setSubmitting、setErrors、resetForm、setStatus等(见 Formik.tsx 第 845 行 的imperativeMethods)。
文档中还有一条与本文主题直接相关的重要说明(formik.md 第 399 行):
如果
onSubmit是 async 的,Formik 会在其 resolve 后自动将isSubmitting置为false,因此你不需要手动调用formikBag.setSubmitting(false);但如果onSubmit是同步函数,你必须自行调用setSubmitting(false)。
两种写法的对比如下:
| 提交函数形式 | 返回值 | 是否需要手动setSubmitting(false) | 适用场景 |
|---|---|---|---|
async (values) => { ... } | Promise | 不需要,Formik 自动处理 | 网络请求、文件上传等真实异步操作 |
(values) => { ...; setSubmitting(false); } | undefined | 必须手动调用 | 纯同步校验后即完成、无异步操作的提交 |
真实场景:把 sleep 替换为 API 请求
将示例中的await sleep(500)替换为真实请求,即可得到生产可用的注册表单:
onSubmit={async (values, { setSubmitting, setStatus }) => { try { setStatus({ message: '正在提交...' }); const res = await fetch('/api/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values), }); if (!res.ok) { // 业务失败:借助 setStatus 或 setErrors 展示错误信息 setStatus({ message: '提交失败,请重试' }); } } catch (error) { setStatus({ message: '网络异常:' + error.message }); } finally { // async 场景下可省略,此处仅示意同步清理的写法 } }}由于onSubmit是 async 函数,无论成功还是失败,Formik 都会在 Promise settle 后自动复位isSubmitting,配合<button disabled={isSubmitting}>即可天然防重复提交。
五、常见问题排查(基于官方 FAQ 提炼)
表单提交流程指南 以 FAQ 形式总结了异步提交中开发者最常遇到的几个问题,与本文示例直接相关,一并整理如下:
Q1:如何判断提交处理器正在执行?当isValidating === false且isSubmitting === true时,说明正处于执行onSubmit的阶段。
Q2:为什么提交前 Formik 会 touch 所有字段?业界惯例是只在字段"被访问过"(touched)后才显示其错误提示。提交前 touch 全部字段,是为了让此前隐藏的错误一次性全部暴露出来。
Q3:如何防止重复提交?在isSubmitting为true时禁用触发提交的控件——即本示例中disabled={isSubmitting}的做法。
Q4:提交后isSubmitting一直是true,怎么办?
- 若提交函数返回 Promise:检查它是否被正确 resolve/reject(例如
await的请求是否永远挂起); - 若提交函数是同步的:检查是否在函数末尾调用了
setSubmitting(false)。
Q5:如何判断表单正在校验?当isValidating === true且isSubmitting === true时,说明校验仍在进行中。
六、总结
通过examples/async-submission示例及其底层源码,可以提炼出 Formik 异步提交的核心范式:
onSubmit写成async函数,内部await网络请求或模拟的异步任务;- 用
isSubmitting禁用提交按钮,杜绝重复提交; - async 提交无需手动
setSubmitting(false),Formik 会在 Promise settle 后自动复位;同步提交则必须手动调用; - 整个提交流程(touch 全部字段 → 校验 → 执行 onSubmit → 复位 isSubmitting)由 Formik.tsx 中的 reducer 与
submitForm协同完成,理解了这三个阶段,就能应对绝大多数提交场景的疑难问题。
如果你想继续深入,推荐阅读仓库中的 form-submission.md(提交流程阶段说明)、formik.md(onSubmit、isSubmitting、submitForm的完整 API 文档),以及 async-submission 同目录下的其他示例(如 async-submission/index.js 对应的 field-level-validation、dependent-fields-async-api-request 等),它们展示了异步逻辑在更复杂表单中的组合用法。
【免费下载链接】formikBuild forms in React, without the tears 😭项目地址: https://gitcode.com/gh_mirrors/fo/formik
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考