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.

20