javascriptreactjsevent-listenerevent-bubblingreact-modal

React popover(modal) closes automatically after I click the open button (event listener)


Goal: I want to implement a popover(modal) in React. When the user click a button, then the popover is open. Close the popover when the user clicks outside of the popover.

Logic: conditionally render the popover component using state. In popover component, add event listener which listen to the click event of the document. If a user clicks a document, it changes the state to false to not render popover component.

Problem: If I click the button to open the popover, event listener triggered. So that the popover closes automatically.

I want to know why this is happening.

import { useState } from "react";
import Popover from "./Popover";

const TaskButton = () => {
  const [isPopoverShown, setIsPopoverShown] = useState(false);

  const popoverOpenHandler = () => {
    setIsPopoverShown(true);
  };

  const popoverCloseHandler = () => {
    setIsPopoverShown(false);
  };

  return (
    <div>
      <button
        onClick={popoverOpenHandler}
      >
        button
      </button>
      {isPopoverShown && (
        <Popover isOpen={isPopoverShown} onClose={popoverCloseHandler}>
          hello
        </Popover>
      )}
    </div>
  );
};

export default TaskButton;

import React, { useEffect, useRef } from "react";

const Popover =({ onClose, children }) => {
  const popoverRef = useRef(null);

  useEffect(() => {
    
    const pageClickEvent = (event) => { 
      if (popoverRef.current !== event.target) { 
        onClose();
      }
    };

    document.addEventListener("click", pageClickEvent); 
    
    return () => {
      document.removeEventListener("click", pageClickEvent);
    };
  }, [onClose]);

  return <div ref={popoverRef}>{children}</div>;
}

export default Popover;

My thought:

I think the problem is coming from event bubbling and capturing.

document.addEventListener("mousedown", pageClickEvent); document.addEventListener("click", pageClickEvent, true);

Because when I change the code one of these, it works how I expected.

But I want to know why it works if I change the event 'click' to 'mousedown'(or up). Or if I change the capture option to true.

I think the event listener is activated only when the component is mounted on DOM. Therefore, the event listener should not be triggered by clicking the button to open the component itself.


Solution

  • You are observing this behavior due to event propagation.

    When you click the button to open the popover, the click event is first captured by the button itself, then bubbles up to the document level. Since your event listener is on the document and it's listening for any click event, it triggers immediately after the button's click event.

    To prevent this behavior, you need to stop the propagation of the button's click event to the document level. You can achieve this by using the e.stopPropagation() method:

    const popoverOpenHandler = (e) => {
      e.stopPropagation();
      setIsPopoverShown(true);
    };
    

    By calling e.stopPropagation() within the popoverOpenHandler, you prevent the click event from propagating beyond the button itself. This means that the document's click event listener won't be triggered by the button click, and your popover won't close immediately when you try to open it.

    Some additional resources that might be helpful: https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation

    I hope this helps.