DEV Community

vibhanshu pandey
vibhanshu pandey

Posted on • Updated on

Click outside listener for React components in 10 lines of code

In this tutorial, you will learn how to create a component that is able to listen on click events outside itself and also BONUS: how to listen for escape keypress.

So you want to listen for click event when the user clicks outside your component. Instead of installing an npm package specifically for it, why not implement it yourself, it'll take less than 10 lines of code.

Let's begin

The first thing you need to know is that you can attach and detach click listeners on the document object itself.

For example:

const clickListener = () => console.log('document clicked');
// Attach a click listener on the document.
document.addEventListener('click', clickListener);

// Detach the click listener on the document.
document.removeEventListener('click', clickListener);
Enter fullscreen mode Exit fullscreen mode

Remember to use a named function or referenced function, like the one used above, not an anonymous arrow function. For example, this will not work:

// Attach a click listener on the document.
document.addEventListener('click', () => console.log('document clicked'));

// Detach the click listener on the document.
document.removeEventListener('click', () => console.log('document clicked'));
Enter fullscreen mode Exit fullscreen mode

Now that we know we can add event listeners on the document object how can we utilize this feature to our advantage?

So Let's start by creating a EnhancedMenu Component that will display a menu when opened and automatically closes itself when clicked outside.

import React, { useState } from 'react'
import { Content } from './Content'

export const EnhancedMenu: React.FC = ({ children }) => {
  const [open, setOpen] = useState(false)

  return (
    <>
      <button
        onClick={() => {
          setOpen(true)
        }}
      >
        Open Menu
      </button>
      {open && (
        <Content
          onClose={() => {
            setOpen(false)
          }}
        >
          {children}
        </Content>
      )}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now let's take a Look at our Content Component.

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

export const Content: React.FC<{ onClose: any }> = ({ onClose, children }) => {
  const ref = useRef(null)
  const escapeListener = useCallback((e: KeyboardEvent) => {
    if (e.key === 'Escape') {
      onClose()
    }
  }, [])
  const clickListener = useCallback(
    (e: MouseEvent) => {
      if (!(ref.current! as any).contains(e.target)) {
        onClose?.() // using optional chaining here, change to onClose && onClose(), if required
      }
    },
    [ref.current],
  )
  // Below is the 10 lines of code you need.
  useEffect(() => {
    // Attach the listeners on component mount.
    document.addEventListener('click', clickListener)
    document.addEventListener('keyup', escapeListener)
    // Detach the listeners on component unmount.
    return () => {
      document.removeEventListener('click', clickListener)
      document.removeEventListener('keyup', escapeListener)
    }
  }, [])
  return (
    <div
      ref={ref}
    >
      {children}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

And That it now click on the EnhancedMenu button and the Content Component will render and attach the click listener on the document object, and when the Content component is unmounted the listener is detached.

One important step to mention is the use of ref in the Content Component. As you may know that Dom events are Propagated upwards in the tree, so to prevent the click event on the Content component itself to close the EnhancedMenu, we check the element that generated that ClickEvent, and if it is not from within the Content component only then we execute the onClose function. see below:

const clickListener = useCallback(
  (e: MouseEvent) => {
    if (!(ref.current! as any).contains(e.target)) {
      onClose?.()
    }
  },
  [ref.current],
)
Enter fullscreen mode Exit fullscreen mode

And that's it, you now have a full function Content Component that you may use anywhere, where you need a click outside listener, or close something when escape key is pressed.

Enjoy.

Top comments (1)

Collapse
 
eddtrdev profile image
eddtrdev

Thanks, exactly what i was looking for!