Search code examples
reactjsexpressmutation-observers

I can't find a good method to reload event listener if the DOM changes


I created a MouseContext, the goal was to play a custom animation on mouse hover.

I want if possible, only use useRef to do that, but I find it a shame to have to put a ref to any element that needs a hover action, for the entire lifespan of my app.

So I tried with manual event listener like this :

  useEffect(() => {
      function inOutHandler(event) {
        event.preventDefault();
        const cursorType = event.type === "mouseenter" ? "active" : "";
        if (
          window.getComputedStyle(event.target)["cursor"].split(", ")[1] ===
          "pointer"
        )
          mouseHandler({ type: "mouseHover", cursorType: cursorType });

        window.removeEventListener("mouseenter", inOutHandler);
        window.removeEventListener("mouseout", inOutHandler);
      }

      const links = document.querySelectorAll(
        "a, .button, button"
      );
      for (let link of links) {
        link.addEventListener("mouseenter", inOutHandler);
        link.addEventListener("mouseout", inOutHandler);
      }
  }, []);

Okay, sound's good, but when a new element is added to the DOM, I have to listen to him again and it's a mess to deal with that.

Again, I tried something, with MutationObserver :

  const mutationObserver = new MutationObserver(async (mutationList, obs) => {
    if (mutationList[0].addedNodes) {
      function inOutHandler(event) {
        event.preventDefault();
        const cursorType = event.type === "mouseenter" ? "active" : "";
        if (
          window.getComputedStyle(event.target)["cursor"].split(", ")[1] ===
          "pointer"
        )
          mouseHandler({ type: "mouseHover", cursorType: cursorType });

        window.removeEventListener("mouseenter", inOutHandler);
        window.removeEventListener("mouseout", inOutHandler);
      }

      const links = document.querySelectorAll(
        "a, .admin .button, button"
      );
      for (let link of links) {
        link.addEventListener("mouseenter", inOutHandler);
        link.addEventListener("mouseout", inOutHandler);
      }
    }
  });

  useEffect(() => {
    if (mainRef.current) {
      mutationObserver.observe(mainRef.current, {
        childList: true,
        subtree: true,
      });

      return () => {
        mutationObserver.disconnect();
      };
    }
  }, [mutationObserver]);

And that's work ! But : The mutation observer is also listening for elements that disappear from the dom, so, with each slide change (for example), my listener are executed twice. And that poses a problem for me in terms of performance.

Do you guys have an advice for me to achieve that please ?


Solution

  • Thanks to jfriend00 :

    It sounds like you may want to use event propagation where you listen for the event on a common parent instead of on the actual DOM element. That way, you can install once listener on a parent and automatically see all the child events, even as DOM elements come and go (as long as the parent you're listening on doesn't change).

    I didn't know that a parent listener would take care of DOM changes. And because I check if the current element event.target is set to pointer, that works only for links and buttons, thank you !

    In terms of performance, this requires testing absolutely all the elements of the app, I don't know if this is very impactful or not.

    The new code :

      function inOutHandler(event) {
        const cursorType = event.type === "mouseenter" ? "active" : "";
        if (
          window.getComputedStyle(event.target)["cursor"].split(", ")[1] ===
          "pointer"
        )
          mouseHandler({ type: "mouseHover", cursorType: cursorType });
    
        mainRef.current.removeEventListener("mouseenter", inOutHandler);
        mainRef.current.removeEventListener("mouseout", inOutHandler);
      }
    
      useEffect(() => {
        if (!loader) {
          mainRef.current.addEventListener("mouseenter", inOutHandler, true); // don't forget the true
          mainRef.current.addEventListener("mouseout", inOutHandler, true);
        }
      }, [loader]);