Hone logo
Hone
Problems

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), and element (React.RefObject<HTMLElement> | null) as arguments.
  • The handler function should be stable across re-renders if possible (e.g., by using useRef internally or by expecting the user to memoize it).
  • The event listener should be added to the element if provided, otherwise to window.
  • The event listener must be removed when the component unmounts.
  • If eventType, handler, or element changes, 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 element ref is initially null?
  • How should the hook handle frequent changes to the eventType or handler?
  • Consider the case where the handler function 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 handler function 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 the handler reference does change.
  • The element provided can be null initially 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 handler function within the useEffect callback. Using useRef to 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 useEffect hook to ensure listeners are added and removed at the correct times.
  • Ensure proper type safety with TypeScript. The eventType should ideally be constrained to valid DOM event types if possible, though a string is acceptable for this challenge. The element type should be generic or specific to HTMLElement.
Loading editor...
typescript