无法在 React useEffect 中访问 DOM 进行清理 Component Unmount 上的 return 函数

Cannot access DOM for cleanup in the react useEffect return function on Component Unmount

提问人:JalajYadav_Avesta 提问时间:10/17/2023 最后编辑:JalajYadav_Avesta 更新时间:10/17/2023 访问量:83

问:

我有以下组件,并通过 DOM 操作将事件列表器附加到其中的按钮。我可以确认 eventListener 已附加,因为单击按钮时,我可以在 devtools 中看到控制台日志。这里的问题是我无法像下面那样在返回功能中删除 eventListener。大多数博客和指南都建议使用 useRef 钩子。我熟悉 useRef 钩子,是的,它确实有效。

import React, { useEffect } from 'react';

function MyCustomComponent() {

  useEffect(() => {
    // Define the event handler
    function handleClick() {
      console.log('Button was clicked!');
    }
    
    document.getElementById("uniqueId").addEventListener('click', handleClick);

    // Cleanup: remove the event listener from the button
    return () => {
      document.getElementById("uniqueId").removeEventListener('click', handleClick);
    };
  }, []); // The empty dependency array means this useEffect will run once after the component is mounted

  return (
    <div>
      <button id="uniqueId">Click Me!</button>
    </div>
  );
}

export default MyCustomComponent;

以上是我面临的问题的缩小版,而不是实际的确切情况。 由于某些特定要求,我在我的项目中提供了一个纯 JS 文件,并尝试使用 dom 操作使用我的 Javscript 代码将 HTML 节点附加到组件中(在这种情况下,假设我使用 dom 操作附加 button#uniqueId),这对我来说工作正常。

但是,当我继续提供清理功能并尝试在清理函数中删除 EventListener 时,我无法访问 DOM 元素,并且它正在添加到 DevTools 中网页的分离节点计数。此问题将增加页面上的负载和网页的潜在内存泄漏。为什么 react 文档建议当我们甚至无法访问其中的 dom 时,可以在返回函数中执行任何清理。

有关该问题的任何进一步澄清,请发表评论。

javascript reactjs dom react-hooks removeeventlistener

评论

0赞 Keith 10/17/2023
您目前正在 React 中渲染按钮,那么这里的自定义 HTML 逻辑在哪里。你的DOM操作会删除这个按钮吗?如果是这样,你在一个痛苦的世界里。如果添加不受 React 控制的动态 HTML,我通常做的是创建一个 ,然后在这个 div 上。然后 React 会保留上面的任何内容,您仍然可以获得您创建的容器。divuseRefdivRef.currentunmountdiv
1赞 T.J. Crowder 10/17/2023
恐怕真的不清楚你的情况。显示的代码是一个简单的组件,用于呈现按钮并处理对按钮的单击。如果你的实际情况比这更复杂,那么该代码并不能帮助你解决它。请用一个最小的可重现示例来演示问题,最好是使用 Stack Snippets(工具栏按钮)的可运行示例。Stack Snippets 支持 React,包括 JSX;这是如何做到的[<>]

答:

2赞 4 revsT.J. Crowder #1

我有以下组件,并通过 DOM 操作将事件列表器附加到其中的按钮。

通常这不是你在 React 中处理事件的方式,请参阅文档。这不仅不必要地复杂,而且如果您有多个组件,则唯一 ID 可能不再是唯一的。

相反,使用 React 来挂接点击处理程序:

function MyCustomComponent() {
    // Define the event handler
    function handleClick() {
        console.log("Button was clicked!");
    }

    // Hook it up with React
    return (
        <div>
            <button onClick={handleClick}>Click Me!</button>
        </div>
    );
}

对于一个简单的元素来说,这可能不是必需的,但是如果你将回调传递给一个子组件,如果其属性没有改变,它可能会避免重新渲染,你可以添加它以使点击处理程序函数稳定。但对于一个简单的人来说,这可能是矫枉过正。buttonuseCallbackbutton


在你的问题中,你似乎在说你的代码可能直接在DOM级别工作。如果是这样,则不清楚代码中显示的组件与您实际执行的操作有何关系。

但是,如果你对 React 应用范围之外的元素执行此操作,以至于你无法处理上述事件,那么你处理这个问题的通常方法是保留对该元素的引用,以便你可以从你附加到的同一个处理程序中删除处理程序:

useEffect(() => {
    // Define the event handler
    function handleClick() {
        console.log("Button was clicked!");
    }
    
    const element = document.getElementById("uniqueId"); // <−−−−−−−−−−−
    element.addEventListener("click", handleClick);

    // Cleanup: remove the event listener from the button
    return () => {
        element.removeEventListener("click", handleClick);
    //  ^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
    };
}, []);

这不仅可以防止在返回时出现问题(因为元素已被删除),还可以防止在元素发生更改时将事件处理程序留在元素上,或者如果事件处理程序暂时与文档分离但又放回原处,等等。您可以保证始终将其从添加它的元素中删除。document.getElementByIdnullid

如果您确定不会更改(通常是这种情况)并且元素不会被分离和重新连接,则可以通过可选链接使用防护装置:idnull

useEffect(() => {
    // Define the event handler
    function handleClick() {
        console.log("Button was clicked!");
    }
    
    document.getElementById("uniqueId").addEventListener("click", handleClick);

    // Cleanup: remove the event listener from the button
    return () => {
        document.getElementById("uniqueId")?.removeEventListener("click", handleClick);
    //                                     ^−−−−−−−−−−−−−−−−−−−−−−−−−−−−
    };
}, []);

不过,我通常会选择第一种方法。

但同样,在答案中显示的代码中,您根本不会使用 、 或 。idgetElementByIdaddEventListener


另一种选择是添加和删除侦听器,而不是特定元素,使用事件委派来确定是否处理点击:body

useEffect(() => {
    // Define the event handler
    function handleClick(event) {
        // Did the element pass through an element with our desired ID?
        const element = event.target.closest("#uniqueId"); // Note the #, that's a CSS selector
        if (element /* If we were doing this on anything other than `body`, we'd want: && event.currentTarget.contains(element)*/) {
            // Yes, handle it
            console.log("Button was clicked!");
        }
    }
    
    document.body.addEventListener("click", handleClick);

    // Cleanup: remove the event listener from the body
    return () => {
        document.body.removeEventListener("click", handleClick);
    };
}, []);

请注意,这将使用具有给定 的当前元素,而不是一开始就具有该元素的元素。(同样,通常不是问题。idid

评论

0赞 T.J. Crowder 10/17/2023
一开始没有正确阅读问题。我现在已经修复了它,以涵盖在 React 和 DOM 中执行此操作,因为目前尚不清楚 OP 的真实情况是什么
0赞 JalajYadav_Avesta 10/17/2023
我实际上是 usign JS dom 操作,用 HTML 节点填充 dom。而且我不能使用 useRef,因为我正在向第三方提供纯 JS 文件。我也在尝试在我提供的清理功能中删除 EventListeners。但是 clenup 功能无法查询 dom,因为它已被删除。
0赞 T.J. Crowder 10/17/2023
@JalajYadav_Avesta - 这并没有让任何事情更清楚。不过,希望上面的一种技术是有用的。
0赞 xgqfrms 10/17/2023
这毫无意义。卸载 时,无需再调用。buttonremoveEventListener
1赞 T.J. Crowder 10/19/2023
@JalajYadav_Avesta - 是的,请参阅文档,特别是:react.dev/reference/react/... (尽管坦率地说,恕我直言,文档曾经更清晰)。