useOnClickOutside
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 monitorcallback
: 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
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:
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.
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:
- First, it ensures the ref's
.current
property exists (the referenced DOM element) - Then it checks if the clicked element (
event.target
) is NOT contained within our referenced element - 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.