Search code examples
javascriptreactjsmaterial-uiuse-effect

ClickAwayListener doesn't fire when clicking on a link/button to navigate to other route


I'm using Material-UI ClickAwayListener component with the existing code that used react-router. The button is outside of the <ClickAwayListener> ... </ClickAwayListener> and so I expected the onClickAway to fire before navigating to other route. But it didn't

Below are the replicate of my code, to some extent to demonstrate what I mean

function Component(){

  const handleClickAway = () => {
    // Do something here
  }

  return (
  <>
    <button>
      <Link to="/other-route">Click here </Link>
    </button>
    // Some other element here
    <ClickAwayListener onClickAway={handleClickAway}>
      <div>
        // Content
      </div>
    </ClickAwayListener>
  </>
  )
}

So if I click any where that is outside of <ClickAwayListener> and <button> the handleClickAway fired, but if I click onto the <button> which contains the like to other route it doesn't.

I tried to look onto source code of ClickAwayListener and this part, I believe, is responsible for detecting the click

 React.useEffect(() => {
    if (mouseEvent !== false) {
      const mappedMouseEvent = mapEventPropToEvent(mouseEvent);
      const doc = ownerDocument(nodeRef.current);

      doc.addEventListener(mappedMouseEvent, handleClickAway);

      return () => {
        doc.removeEventListener(mappedMouseEvent, handleClickAway);
      };
    }

    return undefined;
  }, [handleClickAway, mouseEvent]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.1/umd/react-dom.production.min.js"></script>

As far as I can understand, this part will, first add an event listener to click event when the component mount/re-render and will remove that listener before the component unmount (default behavior for useEffect()). But if this is the case, then before the component get unmount by any event that involved clicking outside of the area of ClickAwayListener, the onClickAway listener should be fired because the listener still attach to the click event.

So in short this is the behavior I expect:

Button click --> onClickAway fire --> component get unmount --> go to new route --> clean-up code of useEffect() run --> the listener get removed

But this is what happen so far

Button click --> component get umount --> go to new route --> clean-up code of useEffect() run --> the listener get removed

Can someone help explain to me why this happen?


Solution

  • ClickAwayListener component works by attaching the event listener to the document, when a mouse event fires, it fires onClickAway only when the mouse event is not inside the element.

    The Link component from react-router-dom essentially renders something like this:

    <a onClick={() => navigate()}>click</a>
    

    When you click the link and call navigate(), React unmounts the current component and mounts the next page component in the next frame. But the thing is, document handlers are only processed after the next re-render, at that point, the event handler from the ClickAwayListener had already been removed since it was unmounted, so nothing get called.

    The problem can be solved by waiting until after the next re-render when the handlers from document have been called.

    <button
      onClick={() => {
        setTimeout(() => {
          history.push("/2");
        });
      }}
    >
    

    Live Demo

    Edit 64531264/clickawaylistener-doesnt-fire-when-clicking-on-a-link-button-to-navigate-to-oth