Debounced Hover on Nested Components Using Event Delegation
Tags: react, eventdelegation, hover, simplicity
Published: 2021-12-10 08:20:42

## Consider the following case:
```typescript
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 `DeepestElement`s)


## One of the approach is
- pass `onMouseEnter` handlers into nested div, with the use of debounce from `lodash-es`
```typescript
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](https://javascript.info/mousemove-mouseover-mouseout-mouseenter-mouseleave#:~:text=Events%20mouseenter%2Fmouseleave%20are%20like,from%20descendants%2C%20are%20not%20counted) and [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseenter_event) for more details)

```typescript
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:
```typescript
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](https://dev.to/thawsitt/should-i-use-event-delegation-in-react-nl0) 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.