React 事件监听踩坑:点一次按钮触发两次请求?原因竟然是这个…

在做项目的时候,经常会遇到一些「看似理所当然」的问题。最近我就在写一个模块时,被一个小问题坑了半天:点一下按钮,居然发了两次请求 🤯。

来,带大家复盘一下我是怎么踩坑、怎么排查,最后又是怎么解决的。


背景

场景很简单:页面里展示了一些markdown文本的信息,我需要把其中的电话号码识别出来,并在后面加个「📞 一键呼出」按钮,方便点击拨打。

于是我写了个 useEffect,在 document 上监听全局点击事件:

erlang 复制代码
useEffect(() => {
  console.log("注册一键呼出事件");
​
  const handleButtonClick = (event: any) => {
    if (event.target.classList.contains("phone-call")) {
      event.preventDefault();
      event.stopPropagation();
​
      const phoneNumber = event.target.getAttribute("data-phone");
      if (phoneNumber) {
        handlePhoneCall(phoneNumber);
      }
    }
  };
​
  document.addEventListener("click", handleButtonClick);
  return () => document.removeEventListener("click", handleButtonClick);
}, []);

按钮是这样插入到内容里的:

bash 复制代码
供应商电话:138****5678 
<button type="button" class="phone-call" data-phone="138****5678">📞一键呼出</button>

结果呢?点一次按钮,触发两次请求!


排查过程

1. 怀疑是事件冒泡

我的第一反应是:是不是点在按钮里的 emoji 也触发了一次?结果打印了一下 event.target,发现并没有,点按钮或者 emoji,都会只触发一次。

2. 怀疑是事件重复绑定

继续排查下去,发现和 React 18 的 StrictMode 有关。

👉 在开发模式下,StrictMode 会让 useEffect 的副作用执行两次: mount → unmount → mount

这意味着我的 document.addEventListener 实际上被绑定了两次。 于是点击按钮就触发了两次。

✅ 问题找到了。


解决方案

方案一:用 useCallback 固定函数引用

我第一步尝试是把 handleButtonClickuseCallback 包起来,避免清理不掉的问题。

ini 复制代码
const handleButtonClick = useCallback((event: MouseEvent) => {
  const target = (event.target as HTMLElement).closest(".phone-call");
  if (target) {
    event.preventDefault();
    event.stopPropagation();
    const phoneNumber = target.getAttribute("data-phone");
    if (phoneNumber) {
      handlePhoneCall(phoneNumber);
    }
  }
}, []);

这样能缓解一点,但 StrictMode 还是会执行两次,体验不算完美。


方案二:事件代理到组件容器(最终 ✅)

最终我选择了更彻底的方案 ------ 事件代理

不在 document 上绑全局事件,而是直接在组件最外层的容器(maven-scroll)上绑定 onClick,利用冒泡去捕获按钮点击:

ini 复制代码
<div
  className="maven-scroll"
  onClick={(e) => {
    const target = (e.target as HTMLElement).closest(".phone-call");
    if (target) {
      e.preventDefault();
      e.stopPropagation();
      const phoneNumber = target.getAttribute("data-phone");
      if (phoneNumber) {
        handlePhoneCall(phoneNumber);
      }
    }
  }}
>
  <MdContent mdContent={preprocessContent(supplierBaseReport?.companyInfo)} />
</div>

这样一来:

  • 点击按钮 → 被捕获,触发一次 ✅
  • 点击容器空白处 → closest 找不到,啥都不发生
  • 不用担心 StrictMode 下的 useEffect 双执行

完美解决 🎉。


最终效果

  1. 内容会被正则处理,手机号后面自动加按钮:

    bash 复制代码
    供应商电话:138****5678 
    <button type="button" class="phone-call" data-phone="138****5678">📞一键呼出</button>
  2. 点击按钮时,日志只输出一次


总结

  1. React 18 严格模式会导致副作用执行两次,如果你在副作用里做了事件绑定,要特别注意是否重复。
  2. 在类似场景下,事件代理比全局事件监听更优雅,逻辑也更可控。
  3. closest('.phone-call') 能确保即使点到按钮里的 emoji 或文字,也能正确识别。
  4. 别忘了给动态生成的 <button> 加上 type="button",避免表单里被当成提交按钮。
相关推荐
এ慕ོ冬℘゜13 分钟前
前端实战:jQuery实现动态表格渲染与状态切换功能
前端·javascript·jquery
jay神1 小时前
【计算机毕业设计】基于Flask+Vue的电影数据可视化与智能推荐系统
前端·vue.js·python·信息可视化·flask·毕业设计·课程设计
FreeTinker7 小时前
HTML本地存储技术解析:localStorage与sessionStorage的原理、差异与应用场景
前端·html
qq_452396237 小时前
第十二篇:《全链路监控与可观测性:前端错误追踪与性能分析》
前端
wangruofeng8 小时前
Phosphor Icons 官网源码拆解:URL 即存储、水波动画与 7362 Star 图标库的架构实践
前端·架构·github
BigTopOne8 小时前
WebRTC Native / Android 开发学习资源整理
前端
excel9 小时前
nuxt 3 升级 nuxt 4 报错之 hasInjectionContext
前端
何以解忧,唯有..10 小时前
LangChain 工具调用(Tool Calling)实战指南
java·前端·langchain
软件聚导航11 小时前
「聚小软 AI 助手」技术升级:从数据库检索到 RAG 知识库问答
前端·数据库·mysql·微信小程序·小程序·ai编程·rag
恋猫de小郭11 小时前
看懂大模型架构术语,帮助你理解目前常见的大模型开源架构
前端·人工智能·ai编程