useOnClickOutside
Prompt
Create a custom hook useOnClickOutside that takes a ref and a callback function. When the user clicks anywhere outside the referenced element, the callback should fire. Clicking inside the element should not trigger it. The hook should clean up its event listener on unmount.
Playground
Start by setting up a useEffect hook with a click event listener on the window object.
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:
trueif elementB is a descendant of elementA (or is elementA itself)falseif 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.
Don't forget to clean up the event listener in the returned function from useEffect to prevent memory leaks.
Solution
Explanation
You'll use this hook all the time in real projects. Every dropdown menu, modal, popover, and tooltip needs the same behavior: if the user clicks somewhere outside of it, close it. Instead of writing that logic in every component, you extract it into a hook once and reuse it everywhere.
export function useOnClickOutside(ref, callback) {
React.useEffect(() => {
function handleClick(event) {
if (
ref.current &&
!ref.current.contains(event.target)
) {
callback();
}
}
window.addEventListener('click', handleClick);
return () =>
window.removeEventListener('click', handleClick);
}, [ref, callback]);
}The clever part is ref.current.contains(event.target). The DOM has a built-in method called contains() that checks if one element lives inside another. So when a click happens anywhere on the page, we ask: "is the thing that was clicked inside our element?" If the answer is no, the click was outside, and we fire the callback.
We listen on window so we catch every click on the page. And as always, we return a cleanup function that removes the listener when the component unmounts.
The dependency array includes both ref and callback. A good practice is for the consumer to wrap their callback in useCallback so the effect doesn't re-run on every render. That's exactly what the demo App component does.
In real apps, this hook powers dropdown menus (click outside to close), modals (click the backdrop to dismiss), date pickers, search autocomplete panels, and just about any overlay UI.