news 2026/9/19 17:47:19

Formik 异步表单提交实战:用 async/await 优雅处理 onSubmit 与 isSubmitting

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Formik 异步表单提交实战:用 async/await 优雅处理 onSubmit 与 isSubmitting

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处理器,并深入剖析isSubmittingsetSubmittingsubmitForm等底层机制——让你不仅能照抄示例,还能理解 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.0react-dom@^16.12.0react-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 内部就能感知"提交尚未完成",从而维持isSubmittingtrue,直到 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>:顶层容器,接收initialValuesonSubmit
  • <Form>:渲染原生<form>元素并自动接管onSubmit/onReset(见 Form.tsx 的实现,它通过useFormikContext()取出handleSubmithandleReset绑定到<form>标签上);
  • <Field>:将输入控件与 Formik 状态绑定(见 Field.tsx,其底层调用getFieldProps注入valueonChangeonBlur到控件上)。

注意initialValues中声明了firstNamelastNameemail三个字段,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, };

即一次完成三件事:

  1. touch 所有字段setNestedObjectValues(state.values, true))——Formik 会在提交前将所有字段标记为"已访问",这样所有隐藏的错误提示都会显示出来;
  2. isSubmitting置为true
  3. 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 异步提交的两个关键设计:

  1. async onSubmit 自动收尾:当onSubmit是 async 函数(返回 Promise)时,Formik 会在 Promise resolve 后自动派发SUBMIT_SUCCESS,把isSubmitting重置为false——因此示例中不需要手动调用setSubmitting(false)
  2. 同步 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 行)会依次执行:

  1. e.preventDefault()阻止浏览器默认提交行为(整页刷新);
  2. e.stopPropagation()阻止事件冒泡;
  3. 调用submitForm()进入上面描述的完整提交流程。

开发环境下它还会检查触发提交的按钮是否缺少type属性,并给出警告提示,以避免 reset 按钮误触发提交的常见坑。

四、onSubmit 的签名与同步/异步对照

根据 API 文档 对onSubmit的定义(formik.md 第 388 行):

onSubmit: (values: Values, formikBag: FormikBag) => void | Promise<any>
  • values:当前表单的字段值对象,与initialValues结构一致;
  • formikBag:一组命令式方法,包括setSubmittingsetErrorsresetFormsetStatus等(见 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 === falseisSubmitting === true时,说明正处于执行onSubmit的阶段。

Q2:为什么提交前 Formik 会 touch 所有字段?业界惯例是只在字段"被访问过"(touched)后才显示其错误提示。提交前 touch 全部字段,是为了让此前隐藏的错误一次性全部暴露出来。

Q3:如何防止重复提交?isSubmittingtrue时禁用触发提交的控件——即本示例中disabled={isSubmitting}的做法。

Q4:提交后isSubmitting一直是true,怎么办?

  • 若提交函数返回 Promise:检查它是否被正确 resolve/reject(例如await的请求是否永远挂起);
  • 若提交函数是同步的:检查是否在函数末尾调用了setSubmitting(false)

Q5:如何判断表单正在校验?isValidating === trueisSubmitting === true时,说明校验仍在进行中。

六、总结

通过examples/async-submission示例及其底层源码,可以提炼出 Formik 异步提交的核心范式:

  1. onSubmit写成async函数,内部await网络请求或模拟的异步任务;
  2. isSubmitting禁用提交按钮,杜绝重复提交;
  3. async 提交无需手动setSubmitting(false),Formik 会在 Promise settle 后自动复位;同步提交则必须手动调用;
  4. 整个提交流程(touch 全部字段 → 校验 → 执行 onSubmit → 复位 isSubmitting)由 Formik.tsx 中的 reducer 与submitForm协同完成,理解了这三个阶段,就能应对绝大多数提交场景的疑难问题。

如果你想继续深入,推荐阅读仓库中的 form-submission.md(提交流程阶段说明)、formik.md(onSubmitisSubmittingsubmitForm的完整 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 17:46:21

缝纫机机械原理课程设计全解析:从机构选型到运动学验证

简介&#xff1a;南航机械原理缝纫机课程设计230.docx是一份面向机械类专业学生的课程设计完整文档&#xff0c;围绕缝纫机导线及紧线机构的设计与运动分析展开。文档从设计题目与原始数据入手&#xff0c;系统完成齿轮传动设计、杆件长度计算、解析法与图解法运动分析、点的运…

作者头像 李华
网站建设 2026/9/19 17:38:43

智慧医院信息化建设方案:从EMPI到集成平台的落地指南

简介&#xff1a;面向医院信息科、弱电智能化设计人员及系统集成商&#xff0c;智慧医院信息化建设方案全面梳理了医疗场景下的智能化与信息化升级路径&#xff0c;覆盖项目总体说明、需求分析、系统设计总则、网络平台建设及软硬件配置等模块&#xff0c;重点解决多子系统协同…

作者头像 李华
网站建设 2026/9/19 17:37:42

BFS算法详解:从原理到实战应用

1. 广度优先搜索&#xff08;BFS&#xff09;算法概述广度优先搜索&#xff08;Breadth-First Search&#xff09;是图论中最基础的遍历算法之一&#xff0c;也是解决许多实际问题的利器。我第一次接触BFS是在大学的数据结构课上&#xff0c;当时就被它那种"层层递进"…

作者头像 李华
网站建设 2026/9/19 17:35:20

Flutter ProgressIndicator 在 OpenHarmony 上的实战:从组件到性能优化

1. 组件认知&#xff1a;ProgressIndicator 解决了什么问题1.1 从用户感知到开发成本的必然选择在 OpenHarmony 生态里做应用开发&#xff0c;加载反馈是躲不开的刚需。页面拉取网络数据、文件读写、图片解码、后台任务提交&#xff0c;这些操作都需要一段时间&#xff0c;如果…

作者头像 李华
网站建设 2026/9/19 17:32:40

Unity真实地形生成:ArcGIS Maps SDK实战与性能优化

1. 为什么要在Unity里折腾真实地形如果你做过数字孪生、飞行模拟、城市规划演示或者户外战术类游戏&#xff0c;一定绕不开一个核心痛点&#xff1a;地形不够真。Unity自带的Terrain系统做个小山包、挖条河还行&#xff0c;但一旦涉及真实地理坐标、大范围地貌、卫星影像贴图&a…

作者头像 李华