Implementing and Understanding the useEventListener React Hook: A Step-by-Step Guide

5 min read

📚 Quick Review: This practical application is built upon a fundamental programming concept. Review the Theory Lesson here first.


The useEventListener hook is a powerful abstraction for managing DOM event listeners in React functional components. It simplifies the process of adding, removing, and ensuring your event handlers always have access to the latest state or props, preventing common issues like memory leaks and stale closures. This lesson will dissect the hook’s implementation line-by-line and demonstrate how to integrate it into your React applications.

The useEventListener Hook: A Glimpse

Here’s the complete code for the useEventListener custom hook:

import { useEffect, useRef } from 'react';function useEventListener(eventName, handler, element = window) {  const savedHandler = useRef();  useEffect(() => { savedHandler.current = handler; }, [handler]);  useEffect(() => {    const isSupported = element && element.addEventListener;    if (!isSupported) return;    const eventListener = event => savedHandler.current(event);    element.addEventListener(eventName, eventListener);    return () => element.removeEventListener(eventName, eventListener);  }, [eventName, element]);}

Line-by-Line Breakdown

Imports: useEffect and useRef

import { useEffect, useRef } from 'react';

We import two fundamental React hooks:

  • useEffect: This hook allows us to perform side effects in functional components. It’s where we’ll attach and detach our event listeners.
  • useRef: This hook provides a way to create mutable values that persist across renders without causing re-renders. It’s crucial for storing a stable reference to our event handler.

Hook Signature and Parameters

function useEventListener(eventName, handler, element = window) {

This defines our custom hook, useEventListener, which accepts three parameters:

  • eventName: A string representing the name of the DOM event to listen for (e.g., ‘click’, ‘resize’, ‘keydown’).
  • handler: The function that will be executed when the event occurs. This is your event callback.
  • element: The DOM element to attach the listener to. It defaults to the global window object, making it convenient for global events. You can pass a ref to a specific DOM element if needed.

Storing the Handler with useRef

  const savedHandler = useRef();  useEffect(() => { savedHandler.current = handler; }, [handler]);

This is a critical pattern for preventing stale closures:

  • const savedHandler = useRef();: We initialize a ref to store our event handler. savedHandler.current will hold the actual handler function.
  • useEffect(() => { savedHandler.current = handler; }, [handler]);: This useEffect runs whenever the handler function (passed as a prop to our custom hook) changes. Its sole purpose is to update savedHandler.current with the latest version of the handler. This ensures that when the event fires, it always calls the most up-to-date handler, even if the original handler function changes across renders.

Attaching and Detaching the Event Listener

  useEffect(() => {    const isSupported = element && element.addEventListener;    if (!isSupported) return;    const eventListener = event => savedHandler.current(event);    element.addEventListener(eventName, eventListener);    return () => element.removeEventListener(eventName, eventListener);  }, [eventName, element]);

This is the main useEffect responsible for the core logic:

  • const isSupported = element && element.addEventListener;: This line performs a basic feature detection. It checks if the provided element exists and if it has the addEventListener method, ensuring the code doesn’t break in environments where it might not be available.
  • if (!isSupported) return;: If the environment or element doesn’t support event listeners, the effect simply exits.
  • const eventListener = event => savedHandler.current(event);: This creates the actual event listener function that will be attached to the DOM. Crucially, it doesn’t directly call the handler prop; instead, it calls savedHandler.current(event). Because savedHandler.current is always updated with the latest handler (thanks to the previous useEffect), this ensures our listener always executes the most current logic.
  • element.addEventListener(eventName, eventListener);: This line attaches the event listener to the specified element.
  • return () => element.removeEventListener(eventName, eventListener);: This is the cleanup function. It’s returned by useEffect and will be executed when the component unmounts or when the dependencies [eventName, element] change. This is vital for preventing memory leaks by removing the event listener when it’s no longer needed.
  • [eventName, element]: This is the dependency array for this useEffect. The effect will re-run (and thus re-attach the listener) only if eventName or the element itself changes. The handler is intentionally excluded from this array because its latest version is already managed by useRef.

How to Use useEventListener in Your Components

Example: Tracking Mouse Clicks

Let’s create a simple component that counts clicks anywhere on the window using our new hook.

import React, { useState } from 'react';import { useEventListener } from './useEventListener'; // Assuming the hook is in useEventListener.jsfunction ClickCounter() {  const [count, setCount] = useState(0);  const increment = () => setCount(prevCount => prevCount + 1);  useEventListener('click', increment); // Listen for 'click' events on the window  return (    

Clicks: {count}

Click anywhere on the window to increment the counter.

);}export default ClickCounter;

Example: Responding to Window Resize

Here’s how you can use the hook to display the current window dimensions, updating them whenever the window is resized.

import React, { useState } from 'react';import { useEventListener } from './useEventListener';function WindowSizeDisplay() {  const [width, setWidth] = useState(window.innerWidth);  const [height, setHeight] = useState(window.innerHeight);  const updateSize = () => {    setWidth(window.innerWidth);    setHeight(window.innerHeight);  };  useEventListener('resize', updateSize); // Listen for 'resize' events on the window  return (    

Window Dimensions

Width: {width}px

Height: {height}px

);}export default WindowSizeDisplay;
💡 Developer Tip: Pay close attention to the dependency arrays of your useEffect hooks. An empty dependency array [] means the effect runs once on mount and cleans up on unmount. If you omit the array, it runs on every render. Incorrect dependencies are a common source of bugs, leading to either stale closures or excessive re-runs of effects. For useEventListener, ensuring eventName and element are in the main useEffect‘s dependency array is vital for correctly re-attaching the listener if these values change, while the handler is managed separately by useRef.

Leave a Reply

Your email address will not be published. Required fields are marked *