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

React严格模式下组件事件监听器如何正确清理?

问题

我正在开发一款需实现可隐藏移动端菜单的Web应用,拥有控制菜单显示与隐藏的toggleMenu函数。当前该功能在生产环境及非严格模式下均可正常工作,但在严格模式下事件监听器会被添加两次且功能失效。

我知晓严格模式会调用componentDidMount两次,且不想禁用它。我已尝试修改代码,在组件卸载时移除事件监听器,但似乎并未成功移除。

原代码如下:

constructor(props) {
    super(props);
    this.state = {
      showMobileMenu: false,
      tabList: [],
    };
  }

  componentDidMount() {
    // Add event listeners to tabs
    let tempTabList = document.getElementsByClassName("react-tabs__tab");

    for (let i = 0; i < tempTabList.length; i++) {
      tempTabList[i].addEventListener("click", this.toggleMenu.bind(this));
    }
    
    this.setState({ tabList: tempTabList });
  }

  componentWillUnmount() {
    // Remove event listeners from tabs
    for (let i = 0; i < this.state.tabList.length; i++) {
      this.state.tabList[i].removeEventListener(
        "click",
        this.toggleMenu.bind(this)
      );
    }
  }

... more code here

我原本期望componentDidMount添加监听器,componentWillUnmount移除,之后再次调用componentDidMount重新添加,但实际并未按预期工作。请问我该如何修改才能使其在严格模式下正常工作?

解决方案

核心问题

你的代码存在两个关键问题:

  1. 函数引用不匹配:每次调用this.toggleMenu.bind(this)都会生成一个全新的函数实例,导致addEventListener和removeEventListener操作的不是同一个函数,监听器无法被正确移除。
  2. DOM元素存储不当:将DOM元素集合存在state中是不合理的,state应仅用于存储组件的状态数据,而非DOM引用。

修复后的代码

基础版本

constructor(props) {
    super(props);
    this.state = {
      showMobileMenu: false,
    };
    // 提前在constructor中绑定this,固定函数引用
    this.toggleMenu = this.toggleMenu.bind(this);
  }

  componentDidMount() {
    // 直接获取DOM元素,存储到实例属性而非state
    this.tabList = document.getElementsByClassName("react-tabs__tab");
    for (let i = 0; i < this.tabList.length; i++) {
      this.tabList[i].addEventListener("click", this.toggleMenu);
    }
  }

  componentWillUnmount() {
    // 使用实例属性中的DOM集合移除监听器
    for (let i = 0; i < this.tabList.length; i++) {
      this.tabList[i].removeEventListener("click", this.toggleMenu);
    }
  }

React范式优化版本(推荐)

如果使用React 16.3+,更推荐用ref来管理DOM元素,避免直接操作全局DOM:

constructor(props) {
    super(props);
    this.state = {
      showMobileMenu: false,
    };
    this.toggleMenu = this.toggleMenu.bind(this);
    // 创建数组存储tab元素的ref
    this.tabRefs = [];
  }

  // 收集tab元素的ref
  addTabRef = (el) => {
    if (el && !this.tabRefs.includes(el)) {
      this.tabRefs.push(el);
    }
  };

  componentDidMount() {
    this.tabRefs.forEach(tab => tab.addEventListener("click", this.toggleMenu));
  }

  componentWillUnmount() {
    this.tabRefs.forEach(tab => tab.removeEventListener("click", this.toggleMenu));
  }

  render() {
    return (
      <div className="tabs-container">
        {/* 假设tab是动态渲染的,给每个tab添加ref */}
        {this.props.tabs.map((tab, idx) => (
          <div
            key={idx}
            className="react-tabs__tab"
            ref={this.addTabRef}
          >
            {tab.label}
          </div>
        ))}
      </div>
    );
  }

效果说明

修改后,严格模式下即使componentDidMount被调用两次,每次组件卸载时都会通过固定的函数引用正确移除监听器,重新挂载时再添加,不会出现重复绑定导致的功能失效问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 09:43:40