← Back toMaster of React ref

Create a custom hook returning a callback ref

Written byPhuoc Nguyen
Created
22 Oct, 2023
Tags
React callback refs
In the previous post, we explored a way to store a node passed to a callback ref as component state. This allows us to reference it easily throughout our component without using complex callbacks or workarounds.
Moreover, creating a custom hook to manage the node and internal states enables us to reuse it across multiple components, making our code more modular and easier to maintain.
To demonstrate how useful this pattern is, we'll walk through real-life examples that have the same functionality: checking whether users click outside of an element. Stay tuned!

Detecting clicks outside an element

In web applications, it's often necessary to detect whether a user has clicked outside an element. For example, a dropdown menu should close when the user clicks anywhere outside of it, or a modal dialog should disappear when the user clicks on the overlay or any area outside of its content.
By detecting these "outside clicks", we can trigger actions and update our component's state accordingly. This behavior is crucial for creating interactive and responsive interfaces that users expect.
In the next section, we'll create a custom hook to detect outside clicks, but first, let's see how we can check if the clicked target is outside a given element in vanilla JavaScript.
To detect whether a user has clicked outside a specific element, we can use an event listener on the `document`. We'll create a function that takes in the event object and checks if the target of the click is inside or outside our desired element.
Here's how we handle the `click` event:
js
const handleClick = (e) => {
if (!ele.contains(e.target)) {
// Clicked outside the element
}
};

document.addEventListener("click", handleClick, true);
It's important to note that we added an event listener to the `document` with capturing set to `true`. This ensures that it runs before any other click handlers on inner elements.
To check clicks against a specific element, we use the `contains()` method. This method helps determine whether the clicked target is inside or outside our element.
Now that you understand how to check if users click outside of a given element in vanilla JavaScript, let's use that knowledge to develop a custom hook that serves the same purpose. Our custom hook will return a callback ref that we can attach to any element we want to check against.
To store the reference of the element, we use a `node` state. This is similar to what we did in the previous post.
tsx
const useClickOutside = (handler: () => void) => {
const [node, setNode] = React.useState<HTMLElement>(null);

const ref = React.useCallback((nodeEle: HTMLElement | null) => {
setNode(nodeEle);
}, []);

return [ref];
};
Our custom hook needs a parameter named `handler` that will execute when users click outside of the element. Now, it's time for our hook to take action and handle the `click` event of the document.
tsx
const useClickOutside = (handler: () => void) => {
const handleClick = React.useCallback((e) => {
if (node && !node.contains(e.target)) {
handler();
}
}, [node]);
};
In this example, the `handleClick` function is used in the `useClickOutside` hook to detect whether a user has clicked outside of a specific element. It takes an event object as input and checks if the clicked target is inside or outside the desired element using the `contains()` method. If the target is outside, the `handler` function passed to the custom hook is called.
To improve performance, we use `useCallback` to memoize the `handleClick` function, so it doesn't get recreated on every render. By passing `[node]` as a dependency array, we ensure that the function always has access to the most recent node reference, which is important because we want to make sure our event listener is always checking against the correct element.
Lastly, we use the `useEffect()` hook to add and remove the `click` event listener of the document when the component mounts and unmounts respectively.
ts
React.useEffect(() => {
document.addEventListener("click", handleClick, true);
return () => {
document.removeEventListener("click", handleClick, true);
};
}, [handleClick]);
We need to pass `handleClick` as a dependency to the `useEffect()` hook because it relies on the current value of `node`, which could change if another element is passed into our custom hook.
To make sure everything stays up-to-date, we pass `[handleClick]` as a dependency array. This ensures that the effect only runs when `handleClick` or any other dependencies change. This avoids unnecessary re-renders and ensures our event listener always uses the most current version of the `handleClick` function.
In order to ensure that our code works on touch devices, we need to add a `touchstart` event listener in addition to the `click` event listener. Some mobile browsers don't support the `click` event on certain elements like `<div>` or `<span>`. Therefore, it's crucial to include the `touchstart` event listener to ensure that our code works seamlessly on all devices.
Here's how we can modify the previous code:
tsx
React.useEffect(() => {
document.addEventListener("touchstart", handleClick, true);

return () => {
document.removeEventListener("touchstart", handleClick, true);
};
}, [handleClick]);
Our hook now works seamlessly on both desktop and mobile devices, making it not only accessible but also more user-friendly.

Automatically closing menus when users click outside

Dropdown menus are a common and useful UI element in web applications. They help us display a list of options or actions in a neat and organized way. When users click on the dropdown button, the menu expands, revealing its contents. This feature is intuitive and user-friendly.
However, once the dropdown menu is open, we need to provide users with a way to close it. One common approach is to add an "X" button or icon that allows users to close the menu explicitly. Another approach is to close the menu automatically when users click outside of it.
In this section, we'll explore how to implement the latter approach using a custom hook that detects clicks outside of an element. By doing so, we can easily trigger actions and update our component's state accordingly.
The `useClickOutside` hook we developed above can also be used for dropdown menus. We can attach it to the dropdown element and pass in a handler function that closes the menu when called.
Now let's see how we can use our custom hook with a simple example of opening and closing a dropdown menu.
tsx
const DropdownMenu = () => {
const [isOpen, setIsOpen] = React.useState(false);

const handleClick = () => setIsOpen(!isOpen);

const closeMenu = () => setIsOpen(false);

const [ref] = useClickOutside(() => {
closeMenu();
});

return (
<div>
<button onClick={handleClick}>Format</button>
{isOpen && (
<div className="dropdown__content" ref={ref}>
<div className="dropdown__body">
<div className="dropdown__item">Bold</div>
<div className="dropdown__item">Italic</div>
<div className="dropdown__item">Underline</div>
...
</div>
</div>
)}
</div>
);
};
The code above demonstrates a `DropdownMenu` component that automatically closes when the user clicks outside of it. When the user clicks on the `Toggle Menu` button, the `isOpen` state is toggled and the menu appears. The hook is utilized to detect when a click occurs outside of the menu, allowing it to close automatically.
Let's take a look at how it works in action:
If you notice that you're using the same logic to maintain the `isOpen` state in multiple places, it might be a good idea to include it in our custom hook.
tsx
const useClickOutside = () => {
const [isOpen, setIsOpen] = React.useState(false);

const handleClick = React.useCallback((e) => {
if (node && !node.contains(e.target)) {
close();
}
}, [node]);

const open = () => setIsOpen(true);

const close = () => setIsOpen(false);

return [ref, isOpen, open, close];
};
The updated version of the hook now includes the `isOpen` state, which tells us whether the target element is open or closed, as well as the `open` and `close` functions that can be used to toggle the element's visibility. We no longer need to pass the handler option, as clicking outside the element will automatically trigger the `close` function and set the `isOpen` state to `false`.
Here's how the dropdown menu uses the updated hook:
tsx
const DropdownMenu = () => {
const [ref, isOpen, open] = useClickOutside();

return (
<div>
<button onClick={open}>Format</button>
{isOpen && (
<div ref={ref}>
...
</div>
)}
</div>
);
};
The dropdown menu in the example below functions similarly to the one introduced in the previous example.

Implementing click outside behavior for modal dialogs

Modal dialogs are an essential UI element that displays content or prompts users for input. They appear in the center of the screen, overlaying other content, providing a focused experience.
One crucial feature of modals is that they should disappear when users click outside of their content. This allows users to dismiss the modal quickly and return to their previous task.
In this section, we'll show you how to use our `useClickOutside` hook to implement this behavior in a simple example of a modal dialog.
tsx
const Modal = ({ children, onClose }) => {
const [ref = useClickOutside(onClose);
return (
<div className="modal">
<div className="modal-content" ref={ref}>
{children}
</div>
</div>
);
};
In this example, we've created a `Modal` component that uses our custom hook to detect clicks outside of its content. When the user clicks outside of the modal's content area, the `onClose` function passed as a prop is called.
Using the `Modal` component is a breeze. Just plug and play!
tsx
const App = () => {
const [isOpen, setIsOpen] = React.useState(false);

const openModal = () => setIsOpen(!isOpen);

const closeModal = () => setIsOpen(false);

return (
<div>
<button onClick={openModal}>Open Modal</button>
{isOpen && (
<Modal onClose={closeModal}>
{/* Content of the modal */}
</Modal>
)}
</div>
);
};
The `App` component is responsible for rendering a button that toggles the visibility of a modal. When the button is clicked, it sets the `showModal` state to true, which causes the modal to appear. The `Modal` component receives the `handleCloseModal` function as a prop and calls it when the user clicks outside of the modal's content area.
Check it out in action:
With our custom hook, implementing a common behavior in our modal dialogs becomes a breeze. This not only makes them more user-friendly but also more accessible.

Conclusion

To sum up, creating a custom hook that returns a callback ref is a powerful technique that allows us to add complex functionality to our components in a reusable and modular way.
By bundling the logic for handling events of a corresponding node, represented by a ref, in a custom hook, we can easily integrate this behavior into any component that requires it. This means we don't have to repeat code or concern ourselves with implementation details.
What's more, by returning extra state and functions from the custom hook, we can further simplify our components and make them more expressive. This leads to cleaner code and easier maintenance in the long run.
Overall, using callback refs with custom hooks is an excellent technique for building flexible and scalable UI components. It's definitely worth adding to your toolkit if you haven't already.

See also

If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn