How to Use React addEventListener for Custom Events: A Complete Guide

Use the native addEventListener API inside a useEffect hook (or componentDidMount in class components) to attach custom event listeners to DOM nodes, and always return a cleanup function to remove the listener on unmount.

React's synthetic event system handles most user interactions through delegated listeners attached to the root container, but when you need to respond to custom DOM events—such as third-party library events, element-specific resize events, or manually dispatched events—you must implement react addeventlistener manually. This guide demonstrates how to efficiently manage native event listeners in React components while preventing memory leaks and performance issues, based on the implementation details found in the facebook/react repository.

Why React addEventListener is Necessary for Custom Events

React's internal event system uses a delegation model where a single listener at the root handles all standard interactions. According to the source code in packages/react-dom/src/events/plugins/SimpleEventPlugin.js, React maintains a registry of known DOM events (like click, change, input) that it can normalize into synthetic events.

Custom events—those not listed in SimpleEventPlugin—bypass this system entirely. The packages/react-dom-bindings/src/events/ReactDOMEventListener.js file shows how React attaches its root-level delegated listeners, but it cannot intercept arbitrary custom events dispatched via new CustomEvent(). Therefore, to listen for these events, you must access the underlying DOM node via refs and call addEventListener directly.

Best Practices for React addEventListener

Preventing Memory Leaks with Cleanup Functions

The most critical aspect of using native event listeners in React is ensuring they are removed when the component unmounts. In functional components, the cleanup function returned from useEffect serves this purpose. In class components, use componentWillUnmount.

Optimizing Performance with Stable References

To prevent unnecessary re-attachments of event listeners, wrap your handler in useCallback to maintain a stable function reference. This ensures the useEffect dependency array remains stable, reducing the frequency of listener teardown and re-creation.

Using Passive Listeners for Scroll Events

For scroll-related events, pass { passive: true } as the third argument to addEventListener. This tells the browser not to block scrolling while waiting for JavaScript execution, significantly improving performance on mobile devices.

Implementation Examples

Functional Components with useEffect

The most common pattern for react addeventlistener in modern React uses the useEffect hook combined with useRef to access the DOM node:

import { useEffect, useRef, useCallback } from 'react';

function CustomEventComponent() {
  const elementRef = useRef<HTMLDivElement>(null);

  const handleCustomEvent = useCallback((event: Event) => {
    const customEvent = event as CustomEvent;
    console.log('Received custom event:', customEvent.detail);
  }, []);

  useEffect(() => {
    const node = elementRef.current;
    if (!node) return;

    node.addEventListener('my-custom-event', handleCustomEvent);

    return () => {
      node.removeEventListener('my-custom-event', handleCustomEvent);
    };
  }, [handleCustomEvent]);

  return (
    <div ref={elementRef}>
      Component listening for custom events
    </div>
  );
}

This approach ensures the listener is attached only once after mount and properly cleaned up before unmount, preventing memory leaks.

Class Components with componentDidMount

For class components, attach listeners in componentDidMount and remove them in componentWillUnmount:

import React from 'react';

class LegacyEventComponent extends React.Component {
  constructor(props) {
    super(props);
    this.containerRef = React.createRef();
    this.handleEvent = this.handleEvent.bind(this);
  }

  componentDidMount() {
    const node = this.containerRef.current;
    if (node) {
      node.addEventListener('custom-action', this.handleEvent);
    }
  }

  componentWillUnmount() {
    const node = this.containerRef.current;
    if (node) {
      node.removeEventListener('custom-action', this.handleEvent);
    }
  }

  handleEvent(event) {
    console.log('Class component received:', event.detail);
  }

  render() {
    return (
      <div ref={this.containerRef}>
        Class-based custom event listener
      </div>
    );
  }
}

Dispatching Custom Events

To trigger these listeners from anywhere in your application or from third-party libraries:

const targetElement = document.getElementById('my-element');
if (targetElement) {
  targetElement.dispatchEvent(
    new CustomEvent('my-custom-event', {
      detail: { timestamp: Date.now(), data: 'payload' },
      bubbles: true,
      cancelable: true
    })
  );
}

Understanding React's Event System Internals

React's synthetic event system is implemented across several key files in the facebook/react repository:

These implementation details confirm that while React's synthetic event system is highly optimized for standard interactions, it intentionally leaves custom event handling to native browser APIs.

Summary

  • Use react addeventlistener only for custom or non-standard DOM events that React's synthetic system cannot handle, such as third-party library events or manually dispatched CustomEvent instances.
  • Always implement cleanup by returning a removal function from useEffect or using componentWillUnmount in class components to prevent memory leaks.
  • Stabilize handler references with useCallback to avoid unnecessary listener re-attachment cycles.
  • Use passive listeners ({ passive: true }) for scroll events to improve performance on mobile devices.
  • Access DOM nodes via refs (useRef or createRef) rather than querying the DOM directly to maintain React's component encapsulation.

Frequently Asked Questions

When should I use react addeventlistener instead of synthetic events?

Use react addeventlistener when handling events that React's synthetic event system does not recognize, such as custom events dispatched via new CustomEvent(), events from third-party libraries that emit native DOM events, or element-specific events like resize on a div (which React does not expose via props). For standard interactions like clicks, form changes, or keyboard events, always prefer React's onClick, onChange, and other synthetic event props.

How do I remove event listeners in react to prevent memory leaks?

In functional components, return a cleanup function from the useEffect hook that calls removeEventListener with the same arguments used in addEventListener. In class components, implement componentWillUnmount and remove the listener there. Failing to remove listeners causes the component's handler function (which may reference component state or props) to remain in memory, preventing garbage collection of the entire component instance.

Can I use react addeventlistener with hooks?

Yes, hooks provide the most ergonomic way to use react addeventlistener. Combine useRef to access the DOM node, useCallback to create a stable handler reference, and useEffect to attach and clean up the listener. This pattern ensures the listener is only attached after the DOM node is available and properly removed when the component unmounts or when dependencies change.

What is the difference between React's onClick and addEventListener?

React's onClick prop uses the synthetic event system, which normalizes event behavior across browsers, implements event pooling (in React 16 and earlier), and delegates events to the root container for better performance. In contrast, addEventListener attaches a native listener directly to the DOM node, bypassing React's normalization and delegation. Use onClick for standard React interactions; use addEventListener only when integrating with non-React code or handling custom events that React does not support.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →