Custom React Hook: useEventListener
Create a reusable React hook called useEventListener that allows components to easily subscribe to and unsubscribe from DOM events. This hook simplifies event handling by abstracting away the manual setup and cleanup required when working with addEventListener and removeEventListener.
Problem Description
Your task is to implement a custom React hook, useEventListener, in TypeScript. This hook should accept an event type (e.g., 'click', 'keydown'), a handler function to be executed when the event occurs, and an optional target element. The hook should manage the lifecycle of the event listener, ensuring it's correctly attached when the component mounts or when the event type or handler changes, and detached when the component unmounts or when the dependencies change.
Key Requirements:
- The hook should accept
eventType(string),handler(function), andelement(React.RefObject<HTMLElement> | null) as arguments. - The
handlerfunction should be stable across re-renders if possible (e.g., by usinguseRefinternally or by expecting the user to memoize it). - The event listener should be added to the
elementif provided, otherwise towindow. - The event listener must be removed when the component unmounts.
- If
eventType,handler, orelementchanges, the old listener should be removed and a new one added. - The hook should be written in TypeScript.
Expected Behavior:
When a component uses useEventListener, the provided handler function should be called precisely when the specified eventType occurs on the target element (or window). When the component unmounts, the event listener should no longer be active.
Edge Cases:
- What happens if the
elementref is initially null? - How should the hook handle frequent changes to the
eventTypeorhandler? - Consider the case where the
handlerfunction itself is not memoized by the user.
Examples
Example 1: Listening for clicks on a button
import React, { useRef } from 'react';
import { useEventListener } from './useEventListener'; // Assuming your hook is in this file
function ClickCounter() {
const buttonRef = useRef<HTMLButtonElement>(null);
const [count, setCount] = React.useState(0);
const handleClick = React.useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []); // Memoized handler
useEventListener('click', handleClick, buttonRef);
return (
<div>
<button ref={buttonRef}>Click me</button>
<p>Clicks: {count}</p>
</div>
);
}
Explanation: The ClickCounter component uses useEventListener to attach a 'click' event listener to the button element referenced by buttonRef. Each time the button is clicked, the handleClick function is executed, incrementing the count. The listener is automatically cleaned up when the component unmounts.
Example 2: Listening for key presses on the window
import React from 'react';
import { useEventListener } from './useEventListener';
function KeyLogger() {
const [lastKey, setlastKey] = React.useState<string | null>(null);
const handleKeyDown = React.useCallback((event: KeyboardEvent) => {
setlastKey(event.key);
}, []);
useEventListener('keydown', handleKeyDown); // No element provided, defaults to window
return (
<div>
<p>Press any key. Last key pressed: {lastKey}</p>
</div>
);
}
Explanation: The KeyLogger component listens for 'keydown' events on the global window object. When a key is pressed, the handleKeyDown function updates the lastKey state with the pressed key. The listener is attached to window because no specific element was provided.
Example 3: Handling a changing handler
import React, { useState, useRef, useCallback } from 'react';
import { useEventListener } from './useEventListener';
function DynamicEventListener() {
const divRef = useRef<HTMLDivElement>(null);
const [message, setMessage] = useState('Initial');
const [isListening, setIsListening] = useState(false);
const handler1 = useCallback(() => {
setMessage('Handler 1 executed');
}, []);
const handler2 = useCallback(() => {
setMessage('Handler 2 executed');
}, []);
const currentHandler = isListening ? handler2 : handler1;
useEventListener('mouseover', currentHandler, divRef);
return (
<div>
<div
ref={divRef}
style={{ width: '100px', height: '100px', backgroundColor: 'lightblue', marginBottom: '10px' }}
>
Hover over me
</div>
<button onClick={() => setIsListening(!isListening)}>
{isListening ? 'Switch to Handler 1' : 'Switch to Handler 2'}
</button>
<p>{message}</p>
</div>
);
}
Explanation: This example demonstrates how the hook should react to changes in its dependencies. When the button is clicked, isListening toggles, causing currentHandler to change. The useEventListener hook should detect this change and re-attach the listener with the new handler function.
Constraints
- The hook must be implemented using React Hooks API (e.g.,
useEffect,useRef). - The
handlerfunction should ideally be stable to prevent unnecessary listener re-attachments if it's not being re-created on every render. However, the hook must correctly handle cases where thehandlerreference does change. - The
elementprovided can benullinitially or if the ref is not yet attached. - The hook should be performant and not introduce unnecessary overhead.
Notes
- Consider how to correctly capture the latest
handlerfunction within theuseEffectcallback. UsinguseRefto store the handler is a common pattern to ensure the listener always calls the most up-to-date version of the handler without needing to re-attach the listener every time the handler function itself changes. - Pay close attention to the dependencies of your
useEffecthook to ensure listeners are added and removed at the correct times. - Ensure proper type safety with TypeScript. The
eventTypeshould ideally be constrained to valid DOM event types if possible, though a string is acceptable for this challenge. Theelementtype should be generic or specific toHTMLElement.