Mar 29, 2024
•
#react
#hooks
•
3 mins read
If you prefer watching instead of reading, I’ve made a video version on my YouTube channel:
In this tutorial, we’re going to build a useClickOutside hook. This hook will allow you to detect a click outside the element, triggering a callback function. Let’s dive in!
First, let’s discuss why do we need that hook. Think of common cases like custom modal dialog, dropdown select menu or your custom context menu. They all require the ability to close when clicked outside. Hence, a reusable function within React is essential to handle such cases efficiently.
To initiate, let’s create a separate file called useClickOutside.tsx and start off with the hook declaration. Our hook will receive an element ref and a callback function to execute.
import { RefObject } from "react";
const useClickOutside = (
elementRef: RefObject<Element>,
callback: () => void
) => {
// our hook code here
};
export default useClickOutside;
Then, we’ll attach a click event listener to the element and implement some logic:
import { RefObject, useEffect } from "react";
const useClickOutside = (
elementRef: RefObject<Element>,
callback: () => void
) => {
useEffect(() => {
// we'll define function here
document.addEventListener("click", handleClickOutside, true);
return () => {
document.removeEventListener("click", handleClickOutside, true);
};
}, [elementRef, callback]);
};
export default useClickOutside;
After that, we define the handleClickOutside function, ensuring that the callback is executed only when the click occurs outside the tracked element:
import { RefObject, useEffect } from "react";
const useClickOutside = (
elementRef: RefObject<HTMLElement>,
callback: () => void
) => {
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (!elementRef?.current?.contains(e.target as HTMLElement) && callback) {
callback();
}
};
document.addEventListener("click", handleClickOutside, true);
return () => {
document.removeEventListener("click", handleClickOutside, true);
};
}, [elementRef, callback]);
};
export default useClickOutside;
However, an observation reveals an issue—the callback is constantly changing each time it’s referenced. This leads to the repetitive addition and removal of event listeners, which isn’t ideal.
To tackle this, let’s employ a more robust approach. By encapsulating the callback function within a useRef and referencing its latest version, we ensure that the event listener is effectively removed only when necessary:
const useClickOutside = (
elementRef: RefObject<HTMLElement>,
callback: () => void
) => {
const callbackRef = useRef<() => void>();
callbackRef.current = callback;
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
!elementRef?.current?.contains(e.target as HTMLElement) &&
callbackRef.current
) {
callbackRef.current();
}
};
document.addEventListener("click", handleClickOutside, true);
return () => {
document.removeEventListener("click", handleClickOutside, true);
};
}, [elementRef, callbackRef]);
};Let’s put our hook to the test. We’re going to attach a ref to our box element and create a function to log out the click event:
import { useRef } from "react";
import "./App.css";
import useClickOutside from "./hooks/useClickOutside";
function App() {
const boxRef = useRef<HTMLDivElement>(null);
useClickOutside(boxRef, () => console.log("click outside"));
return (
<>
<div className="box" ref={boxRef}></div>
</>
);
}
export default App;
Even with nested elements within our box, thanks to event bubbling, our hook remains effective.
That’s it!