How to prevent assigning an event listener multiple times to an element when using Click Outside hooks?

Viewed 1098

There are many versions of the click outside hooks out there that will attach a "mousedown" or "touchstart" event listener to the document and close a dropdown or modal window when you click outside...

The problem is if you have many dropdowns using the same hook you end up with the same events attached to the document multiple times.

How to prevent that? and does it matter as long as you remove them when you unmount the components?

I've already researched and there doesn't seem to be a way to check whether a dom element already has an event attached to it.

An example taken from: useOnClickOutside

const useOnClickOutside = (ref, handler) => {
  useEffect(() => {
    const listener = event => {
      // Do nothing if clicking ref's element or descendent elements
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };

    const escape = event => {
      if (event.keyCode === 27) {
        handler(event);
      }
    };

    // these event listeners pile up on the document if you have several component using that hook
    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);
    document.addEventListener('keydown', escape, false);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
      document.removeEventListener('keydown', escape);
    };
  }, [ref, handler]);
};

Here using that hook on 5 different dropdowns on the page, assigns the same event listener 5 times to the document

enter image description here

2 Answers

The best practice to do this is using useEffect cleanup function

const handleMouseDown = e => {
  console.log(e)
} 

useEffect(() => {
  document.addEventListener("mousedown", handleMouseDown)
  // cleanup
  return () => {
    document.removeEventListener("mousedown", handleMouseDown)
  }
}, [])

By passing the empty array [] to the useEffect you guarantee that the event listeners are registered only in the initial mounting of the component. By using the cleanup function, you avoid registering it more than once.

if we are calling this component multiple times then in that case also on each and every mount event will be attached to the document.

Possible to write useEffect with event listener on top of the component and call handler method from anywhere then it won't happen.

Related