How to Detect a Click Outside of a React Component

With a reusable hook and useRef

JB
JavaScript in Plain English

--

Detecting a click outside a React component is useful for closing dropdowns, modals, and dialogue boxes. It’s a common pattern that clicking outside the body of those components will close them:

Clicking outside of this dropdown menu closes it

I got the main function in this hook from Ben Bud on Stack Overflow. I don’t claim any invention of that code—I modified it, but all credit for the concept goes to his post.

This is the hook: useDetectClickOut.js

import { useEffect, useRef, useState } from 'react';export default function useDetectClickOut(initState) {
const triggerRef = useRef(null); // optional
const nodeRef = useRef(null); // required

const [show, setShow] = useState(initState);
const handleClickOutside = event => {
//if click is on trigger element, toggle modal
if(triggerRef.current &&
triggerRef.current.contains(event.target)) {
return setShow(!show);
}

//if modal is open and click is outside modal, close it
if(nodeRef.current &&
!nodeRef.current.contains(event.target)) {
return setShow(false);
}
};
useEffect(() => {
document.addEventListener("click", handleClickOutside, true);
return () => {
document.removeEventListener("click", handleClickOutside, true);
};
});
return {
triggerRef,
nodeRef,
show,
setShow
}
}

The nodeRef ref is set on whatever we’re opening—sidebar, menu, dialogue box, etc.

The triggerRef is set on whatever triggers the nodeRef. For instance, if the nodeRef is a dropdown menu, the triggerRef is the button that toggles the menu open and closed.

triggerRef is optional—sometimes components aren’t triggered by an element available to an end user, like a dialogue box that warns a user their subscription is expiring in one day. But it’s necessary for components that do have an end user trigger. Otherwise, clicking the triggerRef will register as a click outside, which will automatically close the nodeRef, when sometimes the triggerRef should open the nodeRef.

handleClickOutside is set and removed with useEffect. The cleanup function of useEffect removes the event listener when the component is not rendered. Meanwhile, while the component is rendered, an event listener is set on the document (the entire window) that listens for a click.

On this click, two conditions are checked:

  1. If the triggerRef is rendered and if it’s being clicked. If both conditions are true, show should be set to its opposite. This lets the triggerRef work as a toggle.
  2. If the nodeRef is rendered (ref.current), and if the ref doesn’t contain the clicked element (event.target). If these are met, the click must be outside the ref, and setShow(false) is set.

That is the entire contents of the hook. This is how it could be used in a component:

import React from 'react';
import useDetectClickOut from '../../hooks/useDetectClickOut';
import IconButton from '../raw/IconButton';
import { DrawerTest } from '../../components/DrawerTest';
import { VscEllipsis } from 'react-icons/vsc';
const Dropdown = () => {const { show, nodeRef, triggerRef } = useDetectClickOut(false);return(
<>
<div ref={triggerRef}>
<IconButton icon={<VscEllipsis/>} inline/>
</div>
{show && <Drawer nodeRef={nodeRef}>
<li>{"oats"}</li>
<li>{"oat milk"}</li>
<li>{"oatmeal"}</li>
<li>{"oat bran"}</li>
})}
}
</>
)}
export default Dropdown;

Notice the drawer is only called when show is true. This lets the hook toggle it open and closed.

Because the Drawer is being passed a ref from it’s parent, Dropdown, it must be rendered with a forwardRef . More about forwardRefs in detail here, but this is my super simple Drawer component example using forwardRef

import React, { forwardRef } from 'react';const Drawer = forwardRef((props, ref) => {
return (
<menu className="dropdown" ref={ref}>
{props.children}
</menu>
);
});
export default Drawer;

The dropdown example is just one implementation of the useDetectClickOut hook. It can be re-used for multiple other components in the same application.

--

--