useOnClickOutside

Medium

Prompt

Create a custom React hook called useOnClickOutside that detects clicks outside of a specified element. This hook is commonly used for closing dropdown menus, modals, or popover components when a user clicks outside of them.

This is a practical interview question that tests your understanding of React refs, event handling, and hook cleanup.

Requirements

  • The hook should accept two parameters:
    • ref: A React ref object pointing to the element to monitor
    • callback: A function to call when a click outside the ref element is detected
  • The hook should add a click event listener to the window
  • The hook should check if the click occurred outside the referenced element
  • The hook should properly clean up the event listener when the component unmounts
  • The hook should not trigger the callback when clicking inside the referenced element

Example

function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef();

useOnClickOutside(dropdownRef, () => {
setIsOpen(false);
});

return (
<div>
<button onClick={() => setIsOpen(true)}>Open Menu</button>

{isOpen && (
<div ref={dropdownRef} className="dropdown-menu">
<a href="#">Option 1</a>
<a href="#">Option 2</a>
<a href="#">Option 3</a>
</div>
)}
</div>
);
}

Playground

Hint 1

Start by setting up a useEffect hook with a click event listener on the window object.

Hint 2

Remember to check if the ref's current property exists and if the clicked target is contained within the ref element. You can use the contains method to check if the clicked element is within the ref element.

This is how the contains method works:

// Check if elementA contains elementB
const isInside = elementA.contains(elementB);

The contains method is a built-in DOM API that returns:

  • true if elementB is a descendant of elementA (or is elementA itself)
  • false if elementB is outside of elementA's DOM tree

In our hook, we want to detect clicks OUTSIDE our referenced element, so we use the opposite logic:

// If the ref exists AND the clicked element is NOT inside the ref element
if (ref.current && !ref.current.contains(event.target)) {
// This is a click outside! Call the callback
callback();
}

This approach is much more efficient than manually walking up the DOM tree to check parent-child relationships, and it handles all edge cases like deeply nested elements automatically.

Hint 3

Don't forget to clean up the event listener in the returned function from useEffect to prevent memory leaks.

Solution

Explanation

Our useOnClickOutside hook helps detect when a user clicks outside of a specific element. This is super useful for creating interactive UI components like dropdowns, modals, and popovers that need to close when clicked outside.

Here's how the hook works:

First, we set up an effect using useEffect that will run when the component mounts:

React.useEffect(() => {
// Effect code here
}, [ref, callback]);

Inside this effect, we define a function to handle click events:

function handleClick(event) {
if (
ref.current &&
!ref.current.contains(event.target)
) {
callback();
}
}

This function does a simple but powerful check:

  1. First, it ensures the ref's .current property exists (the referenced DOM element)
  2. Then it checks if the clicked element (event.target) is NOT contained within our referenced element
  3. If both conditions are true, it calls the callback function

Next, we attach this handler to the window's click events:

window.addEventListener('click', handleClick);

And finally, we provide a cleanup function to remove the event listener when the component unmounts:

return () => {
window.removeEventListener('click', handleClick);
};

The dependency array [ref, callback] ensures our effect runs again if either the ref or callback changes.

This pattern is widely used in modern React applications to create UI elements that respond to clicks outside of themselves, providing a better user experience.

Why include both ref and callback in the dependency array? This ensures the hook works correctly if either changes during the component's lifecycle. Using useCallback for the handler function (as shown in the example app) prevents unnecessary re-creation of the effect.

00:00