Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle ref.current.contains() on a children that use ReactDOM.createPortal()?

I create an app using React hooks. I have a helper function onClickOutsideHook(ref, callback) that trigger the callback when you click outside of the component that provide the ref using React.useRef:

export const onClickOutsideHook = (ref, callback) => {
  // Hook get from https://stackoverflow.com/a/42234988/8583669

  React.useEffect(() => {
    const handleClickOutside = event => {
      if (ref?.current && !ref.current.contains(event.target)) {
        callback();
      }
    };
    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [callback, ref]);
};

I have a component Dropdown that use this helper so it close when you click outside of it. This component has a Modal component as children that use ReactDOM.createPortal. I use it to render the Modal in body so it can cover all the app screen. My Modal contain a button that alert a message when you clicked on it:

function Modal() {
  return ReactDOM.createPortal(
    <div
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        height: "100%",
        width: "100%",
        background: "rgba(0,0,0,0.6)"
      }}
    >
      <button onClick={() => alert("Clicked on Modal")}>Click</button>
    </div>,
    document.body
  );
}

function Dropdown(props) {
  const [isModalOpen, setIsModalOpen] = React.useState(false);

  const dropdownRef = React.useRef(null);
  onClickOutsideHook(dropdownRef, props.onClose);

  return (
    <div ref={dropdownRef}>
      Click outside and I will close
      <button onClick={() => setIsModalOpen(true)}>open Modal</button>
      {isModalOpen ? <Modal /> : <></>}
    </div>
  );
}

The problem is when I click on the Modal button to trigger the alert, the Dropdown is closed before since I clicked outside of it (Modal is not rendered as a children of Dropdown but in body). So my alert is never triggered.

Is there a way to define Modal as a children of Dropdown using ref but still render it in body using ReactDOM.createPortal?

Just have a look to the CodeSandbox.

like image 925
johannchopin Avatar asked Jun 29 '20 12:06

johannchopin


People also ask

What is ReactDOM createPortal?

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. ReactDOM. createPortal(child, container) The first argument ( child ) is any renderable React child, such as an element, string, or fragment.

Which component do we have to pass to the ReactDOM render () method?

render() The first argument is the element or component we want to render, and the second argument is the HTML element (the target node) to which we want to append it. Generally, when we create our project with create-react-app , it gives us a div with the id of a root inside index.

How does ReactDOM render work?

Render a React element into the DOM in the supplied container and return a reference to the component (or returns null for stateless components). If the React element was previously rendered into container , this will perform an update on it and only mutate the DOM as necessary to reflect the latest React element.

What is the correct format to create refs in React?

Creating Refs Refs are created using React. createRef() and attached to React elements via the ref attribute. Refs are commonly assigned to an instance property when a component is constructed so they can be referenced throughout the component.


2 Answers

Like the Portals docs says:

Even though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way ...

This includes event bubbling. An event fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree.

But here is not the case, the mousedown event listener was added on the document and not only the Dropdown component. Even so, the bubbling still happens.

This means that if you add a ref to the Modal, and then add an event listener on the mousedown event with the whole purpose to stop the propagation, the handleClickOutside function will never be called.

It may still seem like a workaround, I don't know if there is a proper way to check.

function Modal() {
  const modalRef = useRef();

  useEffect(() => {
    const stopPropagation = e => {
      e.stopPropagation();
    };

    const { current: modalDom } = modalRef;
    modalDom.addEventListener("mousedown", stopPropagation);

    return () => {
      modalDom.removeEventListener("mousedown", stopPropagation);
    };
  }, []);

  return ReactDOM.createPortal(
    <div
      ref={modalRef}
      style={{
        position: "absolute",
        top: 0,
        left: 0,
        height: "100%",
        width: "100%",
        background: "rgba(0,0,0,0.6)"
      }}
    >
      <button onClick={() => alert("Clicked on Modal")}>Click</button>
    </div>,
    document.body
  );
}

Watch the Modal component from the following CodeSandbox.

like image 61
Andrei Avatar answered Sep 27 '22 23:09

Andrei


As a workaround you can add an ID attribute to your modal and then check if the click was outside the modal

function Modal() {
  return ReactDOM.createPortal(
    <div id="modalId">
      <button onClick={() => alert("Clicked on Modal")}>Click</button>
    </div>,
    document.body
  );
}
...
React.useEffect(() => {
  const handleClickOutside = (event) => {
    if (
      ref?.current &&
      !ref.current.contains(event.target) &&
      document.getElementById("modalId") &&
      !document.getElementById("modalId").contains(event.target) // check if click was outside your modal
    ) {
      callback();
    }
  };
  // Bind the event listener
  document.addEventListener("mousedown", handleClickOutside);
  return () => {
    // Unbind the event listener on clean up
    document.removeEventListener("mousedown", handleClickOutside);
  };
}, [callback, ref]);
...
like image 32
Rostyslav Avatar answered Sep 27 '22 23:09

Rostyslav