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

197 Views Asked by At

I have the following component and am attaching an eventlistner to the button present in it by DOM manipulation. I can confirm that the eventListener is attached as when the button is clicked i can see a console log in the devtools.The issue here is i cannot remove the eventListener in the return fucntion as i have done below. Most of the blogs and guidebooks out there suggest using the useRef hook. I am familiar with the useRef hook and Yes it does work.

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;

The above is a minified version the problem i am facing and not the actual exact case. Due to some specific requirement i am serving a pure JS file in my project and trying to append HTML Nodes, to the component using my Javscript code using dom manipulation(in this case imagine i am appending the button#uniqueId with dom manipulation), which is working fine for me.

But when i go onto provid a cleanup fucntion and try to removeEventListener in the cleanup function, I cannot access the DOM elements and it is adding to the Detached Nodes Count for the webpage in the DevTools. This problem will add up to the load onthe page and potential memory Leaks for the webpage. Why does react documentation suggest that any cleanup can be performed in the return fucntion when we cannot even access the dom in it.

For any furthur clarifications on the question, please comment.

1

There are 1 best solutions below

9
On

I have the following component and am attaching an eventlistner to the button present in it by DOM manipulation.

Normally that isn't how you handle events in React, see the documentation. Not only is it unnecessarily complicated, but if you have more than one component, the unique ID may not be unique anymore.

Instead, use React to hook up the click handler:

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>
    );
}

It's probably not necessary for a simple button element, but if you were passing the callback to a sub-component that may avoid re-rendering if its props don't change, you might add useCallback to that to make the click handler function stable. But with a simple button that's probably overkill.


In your question, you seem to be saying that your code may be working directly at the DOM level. If so, it's unclear how the component shown in the code relates to what you're really doing.

But if you were doing this with an element outside your React app's scope such that you can't handle events as above, then the usual way you'd handle this would be to keep a reference to the element so you can remove the handler from the same one you attached it to:

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);
    //  ^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
    };
}, []);

Not only does that prevent a problem if document.getElementById returns null (because the element was removed), but it also prevents leaving the event handler on the element if its id is changed, or if it's temporarily detached from the document but then put back, etc. You're guaranteed you're always removing it from the element you added it to.

If you're sure that the id won't be changed (which is usually the case) and that the element won't be detached and reattached, you could just use a null guard instead via optional chaining:

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);
    //                                     ^−−−−−−−−−−−−−−−−−−−−−−−−−−−−
    };
}, []);

I'd usually go with the first approach, though.

But again, in the code shown in the answer, you wouldn't use an id, getElementById, or addEventListener at all.


Another option is to add and remove a listener on body rather than the specific element, using event delegation to determine whether to process the click:

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);
    };
}, []);

Note that this will use the then-current element with the given id, rather than the one that had that id at the outset. (Again, not usually an issue.)