文章
nextjs 表单提交防止重复hook实现
解决表单在提交过程中,后端接收响应结果跳转页面成功之前间隙,提交按钮再次可点击,用户此时点击了按钮造成重复提交的问题。
1、use-submit-lock.ts
import { useRef, useState } from "react";
export function useSubmitLock() {
const lockedRef = useRef(false);
const [locked, setLocked] = useState(false);
const lock = () => {
if (lockedRef.current) return false;
lockedRef.current = true;
setLocked(true);
return true;
};
const unlock = () => {
lockedRef.current = false;
setLocked(false);
};
return { locked, lock, unlock };
}
2、登陆表单应用
export function LoginForm({
className,
...props
}: React.ComponentProps<"div">) {
const router = useRouter();
const searchParams = useSearchParams();
const nextUrl = searchParams.get("next");
const form = useForm<LoginSchema>({
defaultValues: loginDefaultValues,
resolver: zodResolver(loginSchema),
});
const loginMutation = useLogin();
const { locked, lock, unlock } = useSubmitLock();
const onSubmit: SubmitHandler<LoginSchema> = (data) => {
// 请求中 or 已锁定 → 直接拦截
if (loginMutation.isPending || locked) return;
loginMutation.mutate(data, {
onSuccess: (res) => {
// 登录失败:解锁,允许再次提交
if (!res.success) {
unlock();
return;
}
// 登录成功:锁死直到页面卸载
lock();
const redirectUrl =
nextUrl && nextUrl.startsWith("/")
? nextUrl
: "/dashboard";
router.push(redirectUrl);
},
});
};
const disabled = loginMutation.isPending || locked;
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader>
<CardTitle>登录平台</CardTitle>
<CardDescription>
请输入正确的邮箱和密码登录平台
</CardDescription>
</CardHeader>
<CardContent>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<ControlledInput
name="email"
label="邮箱:"
type="email"
placeholder="m@example.com"
/>
<ControlledInput
name="password"
label="密码:"
type="password"
placeholder="••••••••"
/>
<Field>
<Button type="submit" disabled={disabled}>
{disabled ? "登录中..." : "登录"}
</Button>
</Field>
</FieldGroup>
</form>
</FormProvider>
</CardContent>
</Card>
</div>
);
}3、用户信息更新表单应用
interface UserFormSheetProps {
currentUser: {
id: string;
fullName: string;
email: string;
};
}
export function UserFormSheet({ currentUser }: UserFormSheetProps) {
const [open, setOpen] = useState(false);
const { locked, lock, unlock } = useSubmitLock();
const form = useForm<UpdateUserSchema>({
defaultValues: updateUserDefaultValues,
resolver: zodResolver(updateUserSchema),
});
useEffect(() => {
if (open) {
form.reset({
fullName: currentUser.fullName,
email: currentUser.email,
id: currentUser.id
});
unlock();
}
}, [open, currentUser, form]);
const updateCurrentUserMutation = useUpdateCurrentUser();
const onSubmit: SubmitHandler<UpdateUserSchema> = (data) => {
if (updateCurrentUserMutation.isPending || locked) {
return;
}
lock();
updateCurrentUserMutation.mutate(data, {
onSuccess: () => {
setOpen(false)
},
onError: () => {
unlock();
}
});
};
// React 组件每次渲染都会重新执行UserFormSheet函数体
// 每当 状态(state)或 props 变化 导致组件重新渲染时,disabled 就会重新计算。
// 所以它的值是动态的,会随着 updateCurrentUserMutation.isPending 或 locked 的变化而变化。
// 当 mutation 状态或 locked 变化时,React 会触发重新渲染 → disabled 重新计算 → 按钮禁用状态更新
const disabled = updateCurrentUserMutation.isPending || locked;
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button>编辑信息</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>编辑用户信息</SheetTitle>
<SheetDescription>
修改你的姓名和邮箱信息。点击保存以更新。
</SheetDescription>
</SheetHeader>
<FormProvider {...form}>
<form
id="user-form-sheet"
onSubmit={form.handleSubmit(onSubmit)}
className="grid flex-1 auto-rows-min gap-6 px-4"
>
<div className="space-y-4">
<ControlledInput name="fullName" label="姓名" placeholder="请输入姓名" />
<ControlledInput name="email" label="邮箱" placeholder="请输入邮箱" type="email" />
</div>
</form>
</FormProvider>
<SheetFooter>
<Button
form="user-form-sheet"
type="submit"
disabled={disabled}
>
{disabled ? "保存中..." : "保存"}
</Button>
<SheetClose asChild>
<Button variant="outline">
取消
</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
}文字版的流程图,直观展示 useSubmitLock + mutation 状态 → 按钮 disabled/文本变化 的过程。
用户点击提交按钮
│
▼
┌──────────────────────────┐
│ 检查是否锁定或请求中 │
│ locked || isPending ? │
└─────────┬────────────────┘
│ 是
▼
阻止重复提交
│
└──> 按钮保持 disabled
│
│ 否
▼
┌──────────────────────────┐
│ 调用 lock() 上锁 │
│ mutation.mutate() 提交 │
└─────────┬────────────────┘
│
▼
┌───────────────┐
│ mutation 状态 │
└───────────────┘
│
┌────────┴───────────┐
│ │
成功 失败
│ │
▼ ▼
按钮保持锁定 调用 unlock() 解锁
disabled = true disabled = false
按钮文本: "提交中..." 按钮文本: "提交"
🔹 流程说明
- 提交前检查锁:
- 如果已经
locked或 mutation 正在进行 (isPending) → 拦截,按钮保持禁用
- 如果已经
- 提交时上锁:
- 调用
lock()→locked = true - mutation 发起请求 →
isPending = true
- 调用
- 请求成功或失败:
- 成功:保持锁定(可选择让表单关闭或跳转)
- 失败:调用
unlock()→locked = false,按钮可再次点击
- 按钮状态绑定:
disabled = locked || isPending- 文本根据
disabled改变:disabled ? "提交中..." : "提交"
React 组件状态变化时间线图,展示从点击按钮到请求成功/失败的全过程,直观理解 useSubmitLock 的工作原理。
时间线 ──────────────────────────────►
用户点击提交按钮
│
│ 组件渲染:
│ locked = false
│ isPending = false
│ disabled = false
│ 按钮显示 "提交"
│
▼
提交逻辑触发:
if (locked || isPending) return;
lock() → locked = true
mutation.mutate() → isPending = true
组件重新渲染:
disabled = locked || isPending → true
按钮显示 "提交中..."
▼
mutation 请求进行中:
│ locked = true
│ isPending = true
│ disabled = true
│ 用户无法再次点击
▼
请求完成:
├─ 成功:
│ locked = true (保持锁定)
│ isPending = false
│ disabled = true
│ 按钮显示 "提交中..."(或根据业务显示“成功”)
│ 表单可能关闭或跳转
│
└─ 失败:
unlock() → locked = false
isPending = false
disabled = false
按钮显示 "提交"
用户可重新提交
🔹 流程总结
- 锁定逻辑:
locked防止重复点击 - 请求状态:
isPending表示 mutation 还在进行 - 按钮状态绑定:
const disabled = locked || mutation.isPending;
- 自动同步 UI
- 文本可以直接绑定:
<Button disabled={disabled}>
{disabled ? "提交中..." : "提交"}
</Button>
失败解锁:保证用户在请求失败后可以再次提交
图解版 Hook 防重复提交的组件交互示意图,用箭头和框展示 locked、isPending、按钮状态变化的逻辑关系。
┌──────────────┐
│ 用户点击提交 │
└───────┬──────┘
│
▼
┌───────────────────────┐
│ 检查是否 locked 或 │
│ mutation.isPending │
│ 如果是 → 阻止提交 │
└─────────┬─────────────┘
│ 否
▼
┌───────────────────────┐
│ 调用 lock() 上锁 │
│ locked = true │
│ mutation.mutate() 发起 │
│ 请求 → isPending = true│
└─────────┬─────────────┘
│
▼
┌───────────────────────────┐
│ 组件重新渲染: │
│ disabled = locked || isPending │
│ 按钮显示 "提交中..." │
└─────────┬─────────────────┘
│
▼
┌───────────────┐ ┌───────────────┐
│ mutation 成功 │ │ mutation 失败 │
└───────┬───────┘ └───────┬───────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 锁保持 locked │ │ 调用 unlock() │
│ isPending = false │ │ locked = false │
│ disabled = true │ │ isPending = false │
│ 按钮显示 "提交中..." │ │ disabled = false │
│ 表单关闭或跳转 │ │ 按钮显示 "提交" │
└───────────────┘ └───────────────┘
🔹 图解说明
- 点击提交 → 首先检查
locked || isPending,防止重复提交 - 提交开始 → 调用
lock(),mutation 发起请求,按钮变为禁用 - 请求状态 →
isPending+locked控制按钮禁用和文本 - 请求结束:
- 成功 → 锁保持直到页面跳转/表单关闭
- 失败 → 调用
unlock()解锁,允许用户再次提交