React - `createPortal` with `document.body` causes click event to fire immediately
Asked Answered
A

1

2

My app has a button called Open Modal. When this button is clicked, a modal is appended to the document.body using createPortal. Link to demo

Once the modal has been mounted (i.e. isOpen === true), I would like to print some text to the console each time the user clicks somewhere on the page.

To do this, I'm using a useEffect hook to bind a click event to the document object if isOpen === true:

useEffect(() => {
    const eventHandler = () => console.log("You clicked me");

    if (isOpen) {
      document.addEventListener("click", eventHandler);
    }

  }, [isOpen]);

When the user clicks 'Open Modal' for the first time, isOpen changes from false to true, this causes the component to re-render. Once the re-render is complete, the click event in the above effect is registered.

Problem

Clicking 'Open Modal' for the first time causes the document's click event to fire (you can see the event handler's output in the console). This doesn't make sense to me - how is it possible for the click event on document to already be registered by the time the first 'Open Modal' click has propagated up to it?

Expected behaviour

The first click on 'Open Modal' should only result in the document's click event being registered, the event itself should not fire at this point.

If I omit createPortal (see line 38 of the demo), the app works as expected, i.e. the event is registered on the first click on 'Open Modal' and the event handler is called on all subsequent clicks on the page.

The app also works as expected if I mount the modal to a sibling of the app root instead of document.body (see line 39 of the demo). So the problem described above only occurs when createPortal is used with document.body as the container, but I don't understand why.

Amylaceous answered 21/1, 2021 at 8:16 Comment(0)
G
1

I'm not sure exactly, But I think, the problem occurring because of Event Propagation, By the time propagation completes, document click event is been registered, that might be reason it's calling the eventHandler. Correct me if am I'm wrong.

If you stop the event propagation, it's working fine.

const openModal = useCallback(
    function (e) {
      if (!isOpen) { // stopping the propagation
        e.stopPropagation();
      }
      setOpen(true);
    },
    [setOpen, isOpen]
  );

Demo link here

Governess answered 21/1, 2021 at 10:33 Comment(6)
Thank you @Naren. This could very well be the reason, although I can't wrap my head around what's going on exactly, hopefully someone could shed some more light on that. Why does using createPortal seem to "slow" propagation? Especially if the event is being registered after rendering is complete? The app works as expected without createPortal.Amylaceous
This post provides some insight as to why it's a bad idea to use createPortal with document.body. I don't know enough about native vs synthetic events to explain the behaviour in my post though.Amylaceous
Appending the modal to a sibling of the app root instead of document.body works - see line 39.Amylaceous
Did you check thisGoverness
Yes, I had read that before attempting to use createPortal. If you check out the demo in my last comment, you'll see I'm using their idea of appending the modal to a sibling div of the app root instead of the document.body itself (line 39) - this makes everything work as expected. But I'm still not sure why using createPortal with document.body messes up propagation.Amylaceous
yeah, Adding to child is working fine but not sure what’s wrong with the document.bodyGoverness

© 2022 - 2024 — McMap. All rights reserved.