Debounced Hover on Nested Components Using Event Delegation

Consider the following case:
const FirstLayeredElement = () => (
  <>
    <SecondLayeredElement/>
    ...
  </>
)
const SecondLayeredElement = () => (
  <>
     <DeepestElement1/>
     <DeepestElement2/>
     ...
  </>
)
const DeepestElement1 = () => (
  <span>...</span>
);
const DeepestElement2 = () => (
  <span>...</span>
);
where we want to fire a function logAnalytics()
  • when the cursor is hovered on a DeepestElement for some seconds (e.g. 1s)
  • and we want to know which DeepestElement is captured (Consider some of the info needs to come from the parent components, so we couldn't simply add a listener in DeepestElements)
  • One of the approach is
  • pass onMouseEnter handlers into nested div, with the use of debounce from lodash-es
  • const FirstLayeredElement = () => {
      return (
        <>
          <SecondLayeredElement
            onMouseEnter={(labelType) => logAnalytic(labelType, "Some other info")}
          />
          ...
        </>
      )
    }
    const SecondLayeredElement = ({onMouseEnter}) => {
      return (
         <DeepestElement1
           onMouseEnter={onMouseEnter}
         />
         <DeepestElement2
           onMouseEnter={onMouseEnter}
         />
         ...
      )
    }
    const DeepestElement1 = ({ onMouseEnter }) => {
      // Delay for one second
      const debouncedMouseEnter = onMouseEnter
        ? debounce(onMouseEnter, 1000)
        : undefined;
      return (
        <span
          onMouseEnter={() => debouncedMouseEnter("someLabelType1")}
        >
          ...
        </span>
      );
    };
    const DeepestElement2 = ({ onMouseEnter }) => {
      // Delay for one second
      const debouncedMouseEnter = onMouseEnter
        ? debounce(onMouseEnter, 1000)
        : undefined;
      return (
        <span
          onMouseEnter={() => debouncedMouseEnter("someLabelType2")}
        >
          ...
        </span>
      );
    };
    But seems lots of useless listeners are added...could we do it in a simpler way?
    Event Delegation Approach
  • First we define a hook useDebounceHover, the input onHover will be called onMouseOut if the time difference between onMouseOver and onMouseOut > 1s (onMouseEnter cannot be used in event delegation, check here and here for more details)
  • import { DOMAttributes, MouseEvent, useRef } from "react";
    const ComponentIdToTypeMapping = {
      some_data_id_1: "someLabelType1",
      some_data_id_2: "someLabelType2",
      ...
    }
    const useDebounceHover = <T = Element>(
      onHover?: (event: MouseEvent<T>) => void,
      duration = 1000,
    ): Pick<DOMAttributes<T>, "onMouseOver" | "onMouseOut"> => {
      const labelToHoverDurationMap = useRef({
        some_data_id_1: 0,
        some_data_id_2: 0,
        ...
      });
    
      const handleMouseOver = (event: MouseEvent<T>) => {
        const labelType = ComponentIdToTypeMapping[event.target.dataset.id];
        if (labelType) {
          labelToHoverDurationMap.current[labelType] = Date.now();
        }
      };
    
      const handleMouseOut = (event: MouseEvent<T>) => {
        const now = Date.now();
        const labelType = ComponentIdToTypeMapping[event.target.dataset.id];
        if (labelType) {
          if (
            onHover &&
            now - labelToHoverDurationMap.current[labelType] > duration
          ) {
            onHover(event);
          }
          labelToHoverDurationMap.current[labelType] = 0;
        }
      };
    
      return { onMouseOver: handleMouseOver, onMouseOut: handleMouseOut };
    };
    
    export default useDebounceHover;
  • And so you could:
  • const FirstLayeredElement = () => {
      const { onMouseOver, onMouseOut } = useDebounceHover(logAnalytic);
      return (
        <div 
          onMouseOver={onMouseOver}
          onMouseOut={onMouseOut}
        >
           <SecondLayeredElement/>
           ...
        </div>
      )
    }
    const SecondLayeredElement = () => (
      <>
         <DeepestElement1/>
         <DeepestElement2/>
         ...
      </>
    )
    const DeepestElement1 = () => (
      <span data-id="DeepestElement1">...</span>
    );
    const DeepestElement2 = () => (
      <span data-id="DeepestElement2">...</span>
    );
    The presentation layer should be simpler coz with the hook, we just need to
  • add a parent div for onMouseOver and onMouseOut
  • add data-id to the deepest components
  • Conclusion
    Note that React has done some optimization so the performance w/o event delegation are similar. Event Delegation does not help in performance in React. But for simplicity, actually my team prefers to use Event Delegation.
    But again, there's always trade-off and it depends on different cases ;D.

    26

    This website collects cookies to deliver better user experience

    Debounced Hover on Nested Components Using Event Delegation