20
Debounced Hover on Nested Components Using Event Delegation
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 inDeepestElement
s)
- pass
onMouseEnter
handlers into nested div, with the use of debounce fromlodash-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?
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
andonMouseOut
- add
data-id
to the deepest components
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