You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

react-hook-form多步骤表单点击下一步意外触发submit事件问题

多步骤表单意外触发submit事件的原因及解决方案分析

问题背景

我用react-hook-form构建了一个三步多步骤表单,遇到以下问题:
点击第二步的「下一步」按钮切换到第三步(此时isLastStep变为true),按钮类型从button切换为submit,意外触发了表单的submit事件,直接调用了mutation接口。即使将单个按钮拆分为两个分别渲染,问题仍存在。改用onMouseDown替代onClick后问题解决,想了解原因及该方案是否合理。

原表单JSX代码

<form
  onSubmit={handleSubmit(onSubmit, onInvalidSubmit)}
  className="flex h-full w-full flex-col justify-between"
>
  <AnimatePresence mode="wait">{<step.content />}</AnimatePresence>
  <div className="flex w-full items-center justify-between">
    <div className="">
      <Button
        onClick={back}
        variant="ghost"
        type="button"
        className={`${isFirstStep ? "invisible" : "visible underline"}`}
      >
        Poprzedni krok
      </Button>
    </div>
    <div className="flex items-center">
      <div className="relative mt-4">
        <Button
          disabled={!step.canStep?.()}
          arrow={isLastStep ? undefined : "right"}
          gradient={true}
          type={isLastStep ? "submit" : "button"}
          onClick={() => !isLastStep && next()}
          className="hover:bg-initial"
        >
          {isLastStep && mutationCreateTeacherAcccount.isLoading && (
            <Loader2 className="animate-spin" />
          )}
          {isLastStep ? "Utwórz konto" : "Następny krok"}
        </Button>
      </div>
    </div>
  </div>
</form>

onSubmit回调代码

function onSubmit(data: RegisterTeacherSchemaType) {
  if (isLastStep) {
    mutationCreateTeacherAcccount.mutate(data);
  }
}

拆分按钮后的代码

{
  isLastStep ? (
    <Button
      disabled={!step.canStep?.()}
      gradient={true}
      type="submit"
      className="hover:bg-initial"
    >
      {mutationCreateTeacherAcccount.isLoading && (
        <Loader2 className="animate-spin" />
      )}
      {"Utwórz konto"}
    </Button>
  ) : (
    <Button
      disabled={!step.canStep?.()}
      arrow="right"
      gradient={true}
      type="button"
      onClick={() => next()}
      className="hover:bg-initial"
    >
      Następny krok
    </Button>
  );
}

意外触发submit的原因

这是浏览器事件生命周期+React状态更新后的DOM替换共同导致的:

  • 浏览器的点击事件分为三个阶段:mousedown → mouseup → click。当你点击「下一步」按钮(此时是type="button"),onClick触发next(),React立即更新状态,isLastStep变为true,组件重新渲染,原来的button被卸载,替换成type="submit"的按钮。
  • 此时鼠标还处于按下状态,松开时触发的mouseup和click事件,会落到已经替换完成的submit按钮上。而submit按钮的click事件默认会触发表单的submit行为,因此意外调用了onSubmit里的mutation接口。
  • 拆分按钮后,本质还是状态更新导致DOM元素替换,鼠标松开时的事件依然会触发新submit按钮的默认行为,所以问题无法解决。

用onMouseDown替代onClick的合理性

这个方案是可行的,核心原因如下:

  • mousedown事件在鼠标按下瞬间触发,此时状态还未更新,DOM上仍是原来的type="button"按钮,触发next()完成状态切换后,后续的mouseup和click事件即使落到新的submit按钮上,也不会再触发步骤切换逻辑,且不会触发意外的submit(因为mousedown已经完成了状态更新,后续事件不会关联到步骤切换的逻辑)。
  • 唯一的小瑕疵:如果用户按下鼠标后拖动到按钮外再松开,onMouseDown已经触发了步骤切换,这和常规点击交互(只有松开在按钮上才触发)略有差异,但绝大多数场景下用户不会这么操作,实际影响很小。

更稳妥的替代方案

如果想要完全贴合常规点击交互逻辑,还可以尝试以下几种方法:

  • 保持按钮类型统一为button:所有步骤的按钮都用type="button",第三步的按钮点击时手动调用handleSubmit(onSubmit)()来触发表单提交,完全规避浏览器默认的submit行为。
  • 延迟状态更新:在next()中用setTimeout包裹状态更新逻辑,让浏览器先完成当前的点击事件周期,再更新状态渲染submit按钮。
  • 临时阻止submit默认行为:给第三步的submit按钮添加一个首次渲染的标记(比如用React ref),首次点击时阻止默认行为,之后移除该逻辑,避免意外触发。

内容的提问来源于stack exchange,提问作者Korer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 11:05:24