custom hooks

useOnClickOutside( )

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

Explanation

The custom hook useOnClickOutside serves as a practical tool for managing interactions that occur outside a specified element within React applications. One of its primary use cases is when you need to trigger certain actions or UI changes when a user clicks outside a particular component.

By encapsulating the detection of external clicks, this hook executes the corresponding handler function, enhancing code modularity and reducing boilerplate in components. It listens for user interactions with the DOM and identifies whether they occur outside the referenced element, centralizing the logic for handling such events.

This hook leverages a combination of useRef, useEffect, and useLayoutEffect to ensure consistent performance and correct behavior, even in dynamic React environments. useEffect handles attaching and cleaning up event listeners, ensuring the implementation is both robust and resource-efficient.

Usage Cases

  • Closing Modals: Easily manage modal windows by automatically closing them when clicking outside, enhancing user experience and interaction.
  • Tooltip Visibility Control: Manage tooltip visibility by toggling them open or closed with clicks, while ensuring they automatically close when clicking outside the tooltip area. This enhances UI clarity and provides a seamless user experience.
  • Menu Management: Effortlessly handle menu closure when clicking outside the menu component, providing seamless navigation for users.
  • Popover Interaction: Enhance popover functionality by automatically closing popovers when clicking outside, streamlining user interaction.
  • Dropdown Management: Simplify dropdown behavior by closing it when clicking outside the dropdown area, ensuring smooth user interaction.
  • Dialog Handling: Seamlessly manage dialogs by closing them when clicking outside, offering intuitive dialog interaction for users.

Creation

Code implementation
1const useOnClickOutside = <T extends HTMLElement>(
2 element: RefObject<T>,
3 handler: () => void, // Function to be called when a click outside the element occurs
4 attached = true // Flag to determine whether the event listener should be attached or not
5): void => {
6 // Create a `ref` that stores the latest `handler` function
7 const latestHandler = useRef(handler);
8
9 // Update `latestHandler.current` when the handler changes.
10 // This ensures our `useEffect` below always gets the latest handler without
11 // needing to pass it in its dependencies array,
12 // which would cause the `useEffect` to re-run after every component render
13 // in case the `handler` is created from scratch inside it
14
15 // `useLayoutEffect` is used here to guarantee that the event handler
16 // reference is updated before any subsequent renders or event handling take place.
17 // Since there's a possibility that when using `useEffect`, at a certain moment,
18 // the handler might become outdated. This is because `useEffect` is asynchronous:
19 // its code executes after the browser finishes rendering. Consequently,
20 // the handler will be updated after the browser's initial rendering.
21 // In contrast, `useLayoutEffect` is synchronous, ensuring that its code executes
22 // after all DOM mutations but before the browser updates the interface
23 useLayoutEffect(() => {
24 // Update `latestHandler` when the `handler` (its reference) changes
25 latestHandler.current = handler;
26 }, [handler]);
27 // By using `useRef` and `useLayoutEffect`, this technique eliminates the need
28 // to memoize the handler outside the hook (in the component) via `useCallback`.
29 // It abstracts the optimization logic inside the hook, making it more convenient
30
31 // Set up an effect to attach an event listener to the document
32 // to track clicks outside the specified element.
33 useEffect(() => {
34 // If the event listener should not be attached or there is no valid element, do nothing
35 if (!attached || !element.current) return;
36
37 // Create an event listener that calls the handler function stored in the `ref`
38 const handleClick = (e: Event) => {
39 // If a handler function and a reference to the element are defined,
40 // and the click occurred outside the element, invoke the handler function
41 if (
42 latestHandler &&
43 element.current &&
44 !element.current.contains(e.target as Node)
45 ) {
46 latestHandler.current();
47 }
48 };
49
50 // Attach event listener for `mousedown` event to the `document`
51 document.addEventListener('mousedown', handleClick);
52
53 // Remove the event listener in the cleanup function to ensure proper
54 // resource release and prevent memory leaks associated with the event
55 // listener added during the component's lifecycle
56
57 // The cleanup function is triggered when the component unmounts or
58 // one of the dependencies changes
59 return () => {
60 document.removeEventListener('mousedown', handleClick);
61 };
62
63 // Include `element` and `attached` in the dependency array to ensure the `useEffect` runs when they change.
64 // This guarantees that the event listener will be set up or removed based on changes in these values.
65 // Although we use `latestHandler` inside `useEffect`, it's not necessary
66 // to include it in the dependencies array. `useEffect` knows that the reference to `latestHandler`
67 // doesn't change, so it won't re-run unless `element` and/or `attached` change or the component unmounts
68 }, [element, attached]);
69};
70
71export default useOnClickOutside;
72

Reference

Code implementation
1useOnClickOutside(elementRef, handler, attached?);
2

To utilize the useOnClickOutside hook in a component, simply call it within the component's function body during rendering.

Parameters

NameTypeDescription
elementRefRefObject<HTMLElement>Reference to the DOM element to monitor for clicks outside.
handler() => voidFunction to be called when a click outside the element occurs.
attached (optional)booleanFlag indicating whether the event listener should be attached or not. Default value is `true`.

Return Values

This hook doesn't return any value.

Example Usages

Dropdown Menu with Outside Click Handling

This example demonstrates how to use the custom hook useOnClickOutside to handle menu closure when clicking outside the component. By attaching the hook to the menu component's DOM element reference, it listens for clicks outside the menu and automatically closes it, enhancing user experience and interaction within the React application.

Code implementation
1const menuItems = loadData(3);
2
3const Container: FC = () => {
4 // State to track whether the menu is open or not
5 const [isOpen, setIsOpen] = useState(false);
6
7 // Creating a reference to be attached to the DOM element (menu container)
8 const ref = useRef<HTMLDivElement | null>(null);
9
10 // Function to handle button click to toggle menu visibility
11 const handleToggle = () => {
12 // Toggle the `isOpen` state to the opposite value
13 setIsOpen((prev) => !prev);
14 };
15
16 // Function to handle closing the menu
17 const handleClose = () => {
18 setIsOpen(false);
19 };
20
21 const handleClick = () => {};
22
23 // Call the `useOnClickOutside` hook to handle clicks outside the menu
24 useOnClickOutside(ref, handleClose, isOpen);
25
26 return (
27 <Layer title="Container">
28 <div className="flex items-center justify-center">
29 <div
30 className="relative w-14"
31 ref={ref}
32 >
33 <button
34 onClick={handleToggle}
35 className="rlu__focus relative flex h-14 w-14 items-center justify-center rounded-xl bg-blue-500 text-white"
36 >
37 {/* Render a hamburger or close icon based on `isOpen` state */}
38 {isOpen ? <IconClose /> : <IconOpen />}
39 </button>
40 {/* Conditional rendering of menu based on `isOpen` state */}
41 {isOpen && (
42 <div className="absolute -top-2 left-14 w-40 p-2">
43 <List
44 items={menuItems}
45 onClick={handleClick}
46 />
47 </div>
48 )}
49 </div>
50 </div>
51 </Layer>
52 );
53};
54
An interactive output of the code above

Modal with Outside Click Handling

This example demonstrates how to implement a modal that closes when the user clicks outside of it. The useOnClickOutside custom hook is used to detect clicks outside the modal's content area and trigger the closing function. A ref is created and assigned to the modal's DOM element, allowing the hook to track clicks outside of its boundaries. This approach enhances the user experience by providing an intuitive way to dismiss the modal. Additionally, the modal can be closed using a dedicated "Close Modal" button, offering multiple options for interaction.

Code implementation
1interface ModalProps {
2 onClose: () => void;
3}
4
5const ModalWrapper: FC<ModalProps> = ({ onClose: handleClose }) => {
6 // Creating a reference to be attached to the DOM element
7 const ref = useRef<HTMLDivElement | null>(null);
8
9 // Call the `useOnClickOutside` hook to handle clicks outside the modal
10 useOnClickOutside(ref, handleClose);
11
12 // Call the `useLockBodyScroll` hook to prevent page scrolling when the component is mounted
13 useLockBodyScroll();
14
15 return (
16 <Modal
17 // Assigning the `ref` to the element, allowing it to be tracked by the `useOnClickOutside` hook
18 ref={ref}
19 >
20 <Message>
21 <span>
22 Click the dark area of the screen to close the modal window.
23 </span>
24 <span>Or click the following button:</span>
25 </Message>
26 <Button onClick={handleClose}>Close Modal</Button>
27 </Modal>
28 );
29};
30
31const Container: FC = () => {
32 // State to track whether the modal is open or not
33 const [isOpen, setIsOpen] = useState(false);
34
35 // Function to handle closing the modal
36 const handleClose = () => {
37 setIsOpen(false);
38 };
39
40 // Function to handle opening the modal
41 const handleOpen = () => {
42 setIsOpen(true);
43 };
44
45 return (
46 <Layer title="Container">
47 <div className="flex w-full justify-center">
48 <Button onClick={handleOpen}>Open Modal</Button>
49 </div>
50 {/* Conditional rendering of modal based on `isOpen` state */}
51 {isOpen && <ModalWrapper onClose={handleClose} />}
52 </Layer>
53 );
54};
55
An interactive output of the code above
Last updated on by @skrykateSuggest an improvement on Github repository.