custom hooks

useEventListener( )

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

Explanation

The useEventListener hook enables you to efficiently manage event listeners within React components, preventing memory leaks and unnecessary re-renders. It abstracts away the complexity of adding and removing event listeners to a target element, ensuring clean and concise code. By utilizing this hook, you can register event handlers for various types of events, such as mouse events, keyboard events, or custom events, on a specified element. This hook is particularly useful in scenarios where you need to handle user interactions, respond to browser events, or integrate third-party libraries requiring event handling.

To use the useEventListener hook, you simply invoke it inside your component with the appropriate parameters: the event name, the event handler function, and optionally the target element.

The useEventListener hook simplifies event handling in React applications, streamlines the debugging process, promotes better code organization, and enhances code maintainability.

Usage Cases

  • Interactive Elements: Attach event listeners to global events (e.g., scroll, resize, keydown) or non-interactive elements, making them responsive to user actions.
  • Mouse Tracking: Track mouse movements or gestures, useful for implementing interactive components like sliders or drag-and-drop functionality.
  • Keyboard Shortcuts: Manage keyboard shortcuts to improve accessibility and enhance the user experience.
  • External API Integration: Integrate event-based communication with external libraries or APIs, such as WebSocket connections or custom event systems.
  • Form Validation: Handle input or focus events to validate form fields dynamically.
  • Lazy Loading: Implement event listeners for scroll events, enabling features like lazy loading or infinite scrolling.
  • Custom Event Handling: Encapsulate event-handling logic for reusable components, allowing for custom event flows in your application.

Creation

Code implementation
1const useEventListener = <
2 WEvent extends keyof WindowEventMap,
3 HEvent extends keyof HTMLElementEventMap,
4 DEvent extends keyof DocumentEventMap,
5 T extends HTMLElement = HTMLElement,
6 EventType extends
7 | WindowEventMap[WEvent]
8 | HTMLElementEventMap[HEvent]
9 | DocumentEventMap[DEvent] =
10 | WindowEventMap[WEvent]
11 | HTMLElementEventMap[HEvent]
12 | DocumentEventMap[DEvent],
13>(
14 eventName: WEvent | HEvent | DEvent,
15 handler: (event: EventType) => void,
16 element?: RefObject<T> | Document
17): void => {
18 // Create a `ref` that stores a reference to the latest `handler` function
19 const latestHandler = useRef(handler);
20
21 // Update `latestHandler.current` when the `handler` changes.
22 // This ensures our `useEffect` below always gets the latest handler without
23 // needing to pass it in its dependencies array,
24 // which would cause the `useEffect` to re-run after every component render
25 // in case the `handler` is created from scratch inside it
26
27 // `useLayoutEffect` is used here to guarantee that the event handler
28 // reference is updated before any subsequent renders or event handling take place.
29 // Since there's a possibility that when using `useEffect`, at a certain moment,
30 // the `handler` might become outdated. This is because `useEffect` is asynchronous:
31 // its code executes after the browser finishes rendering. Consequently,
32 // the handler will be updated after the browser's initial rendering.
33 // In contrast, `useLayoutEffect` is synchronous, ensuring that its code executes
34 // after all DOM mutations but before the browser updates the interface
35 useLayoutEffect(() => {
36 // Update `latestHandler` when the `handler` (its reference) changes
37 latestHandler.current = handler;
38 }, [handler]);
39 // By using `useRef` and `useLayoutEffect`, this technique eliminates the need
40 // to memoize the `handler` outside the hook (in the component) via `useCallback`.
41 // It abstracts the optimization logic inside the hook, making it more convenient
42
43 // Set up an effect to attach an event listener to the element
44 useEffect(
45 () => {
46 // Define the listening target
47 const targetElement: T | Document | Window =
48 element instanceof Document
49 ? element
50 : (element?.current ?? window);
51
52 // Check if `addEventListener` is supported
53 if (!('addEventListener' in targetElement)) return;
54
55 // Create an event listener that calls the `handler` function stored in the `ref`
56 const eventListener = (event: EventType): void => {
57 latestHandler.current(event);
58 };
59
60 // Add the event listener
61 targetElement.addEventListener(
62 eventName,
63 eventListener as EventListener
64 );
65
66 // Remove the event listener in the cleanup function to ensure proper cleanup
67 // and prevent memory leaks associated with the event listener
68 // added during the component's lifecycle.
69 // The cleanup function is triggered when the component unmounts or
70 // one of the dependencies changes
71 return () => {
72 targetElement.removeEventListener(
73 eventName,
74 eventListener as EventListener
75 );
76 };
77 },
78
79 // Include `eventName` and `element` in the dependency array to ensure the `useEffect` runs when they change.
80 // Although we use `latestHandler` inside `useEffect`, it's not necessary
81 // to include it in the dependencies array. `useEffect` knows that the reference to `latestHandler`
82 // doesn't change, so it won't re-run unless `eventName` and/or `element` change or the component unmounts
83 [eventName, element]
84 );
85};
86
87export default useEventListener;
88

Reference

Code implementation
1useEventListener(eventName, handler, element?)
2

To utilize the useEventListener hook in a component, simply call it within the component's function body during rendering.

Parameters

NameTypeDescription
eventNameWEvent | HEvent | DEventSpecifies the type of event to listen for (e.g., `click`, `mouseover`). It indicates the event that the handler function will respond to.
handler(event: EventType) => voidThe event handler function that will be called when the specified event occurs. It defines the behavior or action to be executed in response to the event.
element (optional)RefObject<T> | DocumentSpecifies the target element to which the event listener will be attached. It can be either a direct reference to a DOM element or a React `ref` object obtained using the `useRef` hook. If not provided, the event listener will be attached to the `window` object by default.

Return values

This hook doesn't return any value.

Example Usages

Tab Visibility Analytics

This example demonstrates the use of the useEventListener hook to track tab visibility changes. The event listener is attached to the document, ensuring that visibility changes across the entire page are captured. With the visibilityChangeHandler function, analytics data is dispatched when the user switches tabs, leveraging the visibilitychange event. This approach facilitates comprehensive user engagement monitoring within web applications, as it covers all visibility alterations across the document.

Code implementation
1// Define function to handle the visibility change event, performing actions such as
2// sending analytics data when the user leaves or returns to the tab
3const visibilityChangeHandler = () => {
4 if (document.visibilityState === 'hidden') {
5 // Your logic to perform analytics data sending to the server
6 // when the user leaves the tab
7
8 console.log(
9 `You left the tab at ${new Date().toLocaleTimeString()} so analytic data was sent to the server.`
10 );
11 } else {
12 // Optionally, you can implement logic for sending analytics data
13 // to the server when the user returns to the tab
14 console.log(
15 `You returned to the tab at ${new Date().toLocaleTimeString()}.`
16 );
17 }
18};
19
20const url =
21 'https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event';
22
23const Tracker: FC = () => {
24 // Attach an event listener for the `visibilitychange` event to the document
25 useEventListener('visibilitychange', visibilityChangeHandler, document);
26
27 return (
28 <Layer title="Tracker">
29 <Text>
30 Go to another tab by <br />
31 <Link
32 href={url}
33 rel="noreferrer"
34 target="_blank"
35 >
36 visiting MDN
37 </Link>
38 , then come back and take a look at the console.
39 </Text>
40 </Layer>
41 );
42};
43
An interactive output of the code above

Click Tracking

This example demonstrates click tracking functionality using the useEventListener hook, leveraging useRef to reference the link element. The handleLinkClick callback increments the click count stored in state. By attaching an event listener for the click event to the HTML element via useRef hook, interactions are monitored, and the click count is updated accordingly. This approach optimizes performance by efficiently managing event handling, enhancing the responsiveness of user interface elements within web applications.

Code implementation
1const id = 'example-div';
2
3const initialValue = 0;
4
5const Tracker: FC = () => {
6 const url = `#${id}`;
7
8 // Create a `ref` to store a reference to the link element
9 const linkRef = useRef<HTMLAnchorElement | null>(null);
10
11 // Initialize state to track the number of link clicks
12 const [linkClicks, setLinkClicks] = useState(initialValue);
13
14 const handleLinkClick = () => {
15 setLinkClicks((prevCount) => prevCount + 1);
16 };
17
18 // Attach an event listener for the `click` event to the link element,
19 // incrementing the click count when the link is clicked
20 useEventListener('click', handleLinkClick, linkRef);
21
22 return (
23 <Layer title="Tracker">
24 <Text>
25 Click this link:{' '}
26 <Link
27 // Assigning the `ref` to the element, allowing it
28 // to be tracked by the `useEventListener` hook
29 ref={linkRef}
30 href={url}
31 >
32 Click Me!
33 </Link>
34 </Text>
35 <Message id={id}>You get here by clicking the link above.</Message>
36 <Text>Number of clicks: {linkClicks}</Text>
37 </Layer>
38 );
39};
40
An interactive output of the code above

Internet Connection Status Monitoring

This example showcases internet connection status monitoring using the useEventListener hook. The handleOnline and handleOffline callbacks toggle the isOnline state, reflecting the current internet connectivity. By attaching event listeners for the online and offline events directly to the window, changes in connection status are tracked. This setup allows for dynamic UI updates, such as enabling/disabling input fields and buttons based on the internet connection status, enhancing user experience in web applications.

Code implementation
1const Tracker: FC = () => {
2 // Initialize state to track the online/offline status
3 const [isOnline, setIsOffline] = useState(true);
4
5 // Define callback functions using `useCallback` to toggle the `isOnline` state
6 const handleOnline = useCallback(() => {
7 setIsOffline(true);
8 }, []);
9
10 const handleOffline = useCallback(() => {
11 setIsOffline(false);
12 }, []);
13
14 // Attach event listeners for the `online` and `offline` events to track
15 // changes in the internet connection status, updating the state of `isOnline` accordingly.
16 // Omit specifying an element to attach the handler directly to the window
17 useEventListener('online', handleOnline);
18 useEventListener('offline', handleOffline);
19
20 return (
21 <Layer title="Tracker">
22 <Text>
23 Internet status:{' '}
24 <span className={isOnline ? 'text-green-500' : 'text-red-500'}>
25 {isOnline ? '✅ Connected' : '❌ Disconnected'}
26 </span>
27 </Text>
28
29 <div className="flex flex-col gap-2">
30 <Label>Important Message</Label>
31 {/* Input field will be disabled if the internet connection is lost */}
32 <Input disabled={!isOnline} />
33 <div className="flex w-full justify-center">
34 {/* Button will be disabled if the internet connection is lost */}
35 <Button disabled={!isOnline}>Save progress</Button>
36 </div>
37 </div>
38 </Layer>
39 );
40};
41
An interactive output of the code above
Last updated on by @skrykateSuggest an improvement on Github repository.