Click outside listener for React components in 10 lines of code
Tags: react, javascript, typescript, node
Published: 2020-01-06 12:39:01

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:
```js
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);
``` 

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:

```js
// 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'));
``` 

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.

```js
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>
      )}
    </>
  )
}
``` 

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

```js
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>
  )
}
```

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:

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

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.