别再手动写 loading 了!封装一个自动防重提交的 Hook 每次提交表单都要写loading true、disabled true、.finally(() loading false)你不是在写业务你是在重复造轮子。在日常开发中我们无数次面对这样的场景用户点击“提交订单”点击“发送验证码”点击“保存设置”而为了防止重复点击你不得不定义一个loading状态在点击时设为true禁用按钮发起请求成功或失败后再设回false。一段逻辑复制粘贴十次。更糟的是——一旦忘记写.finally按钮就永远禁用一旦并发请求没处理好照样重复提交。今天我们就用一个自定义 Hook彻底终结这种体力劳动。手动管理 loading 的三大痛点1.代码冗余const [submitting, setSubmitting] useState(false); const handleSubmit async () { if (submitting) return; setSubmitting(true); try { await submitForm(); } finally { setSubmitting(false); // 忘记这行按钮就废了 } };每个按钮都要写一遍毫无意义。2.无法天然防重即使你写了if (submitting) return如果用户快速双击在setSubmitting(true)异步更新前两次点击仍可能触发两次请求。3.状态分散难以维护多个按钮多个表单每个都要独立管理状态逻辑割裂。解法封装一个useSubmitLockHook我们要实现的效果const [handleSubmit, isSubmitting] useSubmitLock(async (formData) { await api.submitOrder(formData); message.success(下单成功); }); return ( button disabled{isSubmitting} onClick{() handleSubmit(data)} {isSubmitting ? 提交中... : 立即下单} /button );一行调用自动加锁、自动解锁、自动防重、自动透传参数实现原理Promise 锁 状态同步// React TypeScript 版本JS 可轻松转写 import { useState, useCallback } from react; type AsyncFunctionT extends any[], R (...args: T) PromiseR; export const useSubmitLock T extends any[], R( asyncFn: AsyncFunctionT, R ) { const [isLocked, setIsLocked] useState(false); const wrappedFn useCallback( async (...args: T): PromiseR | undefined { if (isLocked) { console.warn(操作正在进行中请勿重复提交); return; // 直接拦截不执行函数 } setIsLocked(true); try { const result await asyncFn(...args); return result; } finally { setIsLocked(false); // 无论成功失败一定解锁 } }, [isLocked, asyncFn] ); return [wrappedFn, isLocked] as const; };关键设计亮点特性说明闭包锁isLocked 为 true 时直接 return不执行原函数自动 finally 解锁即使接口报错、用户中断也不会卡死泛型支持完美透传参数和返回值类型无副作用不依赖全局状态每个调用独立隔离使用场景全覆盖场景 1表单提交const [submitForm, submitting] useSubmitLock(api.createPost);场景 2发送验证码const [sendCode, sending] useSubmitLock(phoneApi.sendSmsCode); // 按钮文案可结合倒计时{sending ? 发送中... : 获取验证码}场景 3删除确认操作const [confirmDelete, deleting] useSubmitLock(api.deleteUser); // 防止用户狂点“确定”导致多次删除场景 4组合多个异步操作const [handlePay, paying] useSubmitLock(async (orderId) { await api.createPayment(orderId); await trackEvent(pay_clicked); window.location.href /payment; });注意事项 进阶建议1.不要用于需要“取消”的操作此 Hook 适用于“提交即不可逆”的场景。如果是上传、下载等可取消任务应使用AbortController。2.与防重 Token 不冲突useSubmitLock是前端体验层防护后端仍需配合 Token 或幂等设计做最终校验。3.Vue 用户怎么办同样可封装为 Composable// Vue 3 Composition API import { ref } from vue; export function useSubmitLock(asyncFn) { const isLocked ref(false); const wrappedFn async (...args) { if (isLocked.value) return; isLocked.value true; try { return await asyncFn(...args); } finally { isLocked.value false; } }; return { execute: wrappedFn, isLocked }; }使用const { execute: submit, isLocked } useSubmitLock(api.submit);更进一步自动绑定到按钮你可以再封装一个SubmitButton组件const SubmitButton ({ onClick, children, ...props }) { const [handler, loading] useSubmitLock(onClick); return ( button disabled{loading} onClick{handler} {...props} {loading ? 处理中... : children} /button ); }; // 使用 SubmitButton onClick{submitOrder}提交订单/SubmitButton从此防重提交零成本集成。结语优秀的工程师不是写更多代码而是让重复的事不再发生。一个小小的useSubmitLock背后是对用户体验的尊重对代码洁癖的坚持更是对“DRY 原则”的践行。下次当你又要写第 101 次loading true时停下来问问自己“这事能不能一次解决”把这个 Hook 加到你的工具库里团队效率提升 10%。