Clean Up jQuery Event Listeners in React on Unmount

Integrating legacy jQuery code or plugins into modern React applications requires careful management to prevent memory leaks and unexpected side effects. Because React uses a virtual DOM while jQuery interacts directly with the real DOM, React will not automatically remove jQuery listeners when a component is removed from the DOM tree. This guide explains how to properly detach jQuery event listeners when a component unmounts using both modern functional components with hooks and legacy class components, alongside best practices like event namespacing.

Functional Components with useEffect

In modern React, side effects such as attaching DOM event listeners are handled inside the useEffect hook. To clean up listeners upon unmounting, you must return a cleanup function from the hook.

Using React's useRef ensures you are querying the exact DOM node associated with the specific component instance rather than selecting elements globally.

import React, { useEffect, useRef } from 'react';
import $ from 'jquery';

function JQueryButton() {
  const buttonRef = useRef(null);

  useEffect(() => {
    const $button = $(buttonRef.current);

    // Attach event listener with a namespace
    $button.on('click.myComponent', () => {
      console.log('Button clicked via jQuery');
    });

    // Cleanup function executes on component unmount
    return () => {
      $button.off('click.myComponent');
    };
  }, []); // Empty dependency array ensures this runs on mount and unmount only

  return <button ref={buttonRef}>Click Me</button>;
}

export default JQueryButton;

Class Components with componentWillUnmount

If you are working with legacy React class components, event listeners are attached in the componentDidMount lifecycle method and cleaned up in componentWillUnmount.

import React, { Component } from 'react';
import $ from 'jquery';

class JQueryButtonClass extends Component {
  constructor(props) {
    super(props);
    this.buttonRef = React.createRef();
  }

  componentDidMount() {
    this.$button = $(this.buttonRef.current);
    
    // Attach event listener
    this.$button.on('click.myClassComponent', this.handleClick);
  }

  componentWillUnmount() {
    // Detach event listener
    this.$button.off('click.myClassComponent');
  }

  handleClick = () => {
    console.log('Button clicked in class component');
  };

  render() {
    return <button ref={this.buttonRef}>Click Me</button>;
  }
}

export default JQueryButtonClass;

Best Practices for jQuery Cleanup

1. Always Use Event Namespacing

When attaching events with jQuery's .on(), always append a namespace (such as click.customNamespace). Using namespaces allows you to call .off('click.customNamespace') to remove only the listeners your component added, avoiding the accidental removal of handlers attached by other components or third-party libraries.

2. Clean Up Global Window or Document Listeners

If your component attaches events to $(window) or $(document), unmounting without cleanup creates major memory leaks and ghost interactions. Apply the same pattern using namespaces:

useEffect(() => {
  $(window).on('resize.componentScope', handleResize);

  return () => {
    $(window).off('resize.componentScope');
  };
}, []);

3. Destroy Third-Party Plugins

If you are initializing a complex jQuery plugin (such as Select2, DataTables, or Slick Carousel), detaching event listeners manually might not be enough. Most plugins provide an internal teardown method—often named .destroy()—which removes internal event handlers and resets modified DOM elements automatically:

useEffect(() => {
  const $element = $(elementRef.current);
  $element.pluginName();

  return () => {
    if (typeof $element.pluginName === 'function') {
      $element.pluginName('destroy');
    }
  };
}, []);