Prompts-MCP 686 skills · ai / design / design-pattern / framework / fundamentals / habit / lang / tech-selection /mcp/sse
framework framework/react/reliability/prevent-double-submit.md medium

react-prevent-double-submit

提交类操作前端防重复提交 — 用 isPending 锁住按钮 + 非幂等带 Idempotency-Key。Use when 写表单/下单/支付提交 / 按钮被连点重复请求 / 评审 onClick 直接 await 无锁。

防重复提交double-submitidempotency-keyispending按钮 loading 锁submit
paths
  • frontend/src/**/*.tsx
  • frontend/src/**/*.ts

React · 防重复提交

规则

决策点:提交类操作(下单/支付/创建)必须在前端加“提交中态锁”防双击,但去重的最终保证在后端。三招:

招式 做法 说明
按钮锁 <Button loading={isPending} disabled={isPending}> 提交期间禁用,UI 层挡住连点
锁源 useMutationisPending 当锁 不要自己 useState 管 loading,状态机交给 Query
幂等键 非幂等操作请求头带 Idempotency-Key 同一键重试只生效一次,后端识别

前端只做 UI 层防抖;真正幂等去重靠后端(见相关),不可只靠前端锁。

反例 — 裸 onClick 直接 await,无锁

// ❌ 双击=两次下单:没有提交中态,按钮始终可点
function PayButton({ orderId }: { orderId: string }) {
  return (
    <Button
      type="primary"
      onClick={async () => {
        await ordersApi.pay(orderId); // 第一次未返回前可再次点击
        message.success("支付成功");
      }}
    >
      支付
    </Button>
  );
}

正例 — isPending 锁 + Idempotency-Key

幂等键生成下沉到 util 纯函数:

// src/utils/idempotency.ts
export function newIdempotencyKey(): string {
  // 每次发起一笔提交生成一个唯一键,重试复用同一键
  return crypto.randomUUID();
}
import { Button, message } from "antd";
import { useMutation } from "@tanstack/react-query";
import { newIdempotencyKey } from "@/utils/idempotency";
import { ordersApi } from "@/api/orders";

function PayButton({ orderId }: { orderId: string }) {
  // 锁源:isPending 由 useMutation 维护,不自己 useState 管 loading
  const { mutate, isPending } = useMutation({
    mutationFn: (key: string) =>
      ordersApi.pay(orderId, { headers: { "Idempotency-Key": key } }),
    onSuccess: () => message.success("支付成功"),
    onError: () => message.error("支付失败,请重试"),
  });

  // 编排:本次提交生成幂等键并发起
  const handlePay = () => mutate(newIdempotencyKey());

  // loading + disabled 双重锁住按钮,提交期间挡住连点
  return (
    <Button type="primary" loading={isPending} disabled={isPending} onClick={handlePay}>
      支付
    </Button>
  );
}

表单场景同理:<Form onFinish><Button htmlType="submit" loading={isPending} disabled={isPending}>

自检

  • [ ] 提交按钮 loading={isPending}disabled={isPending}
  • [ ] 锁源用 useMutationisPending,没有自己 useState 管 loading?
  • [ ] 非幂等操作(下单/支付/创建)请求头带 Idempotency-Key
  • [ ] 没有出现裸 onClick={async () => await ...} 无锁提交?
  • [ ] 清楚前端锁只防双击,去重最终靠后端幂等?

相关