How to create a reusable useOnClickOutside hook in react?
A custom hook to detect the mouse or touch event on the outside of specified elements
Table of contents
From the time Hooks were introduced in React 16.8, it has changed how react is written in its ecosystem. Hooks are functions that let you “hook into” React state and lifecycle features from functional components. Hooks make React so much better because you have simpler code that implements similar functionalities faster and more effectively. In today's blog, we will look at how to create a useOnClickOutside
hook in react.
Purpose of the hook
While creating UI components in react, we might have come across situations where we want some action to happen if the user clicks outside of a certain element. E.g In modals, if a user clicks on the overlay the modal closes.
We will see how we can achieve that with hooks and make it re-usable. Let's Go 🚀
Requirments
Before writing any code let's understand what needs to be done. Let's list it down one by one.
- We want to create a hook (
useOnClickOutside
) that should detect if a user clicks or touches (Touch-enabled devices) on elements other than the specified ones and should trigger a function. - Hook should also accept a list of elements that should not trigger the hook. this will be useful if we have multiple elements which should be excluded.
To build this it will require two parameters:-
handler
- A callback function to be executed when the event occurs.nodes
- A array of elements that should not trigger the hook. this can be an optional parameter.
Let's code
import { useEffect, useRef } from "react";
const DEFAULT_EVENTS = ['mousedown', 'touchstart'];
export const useOnClickOutside = ({ handler, nodes }) => {
const element = useRef()
return element
}
We first defined the events we need in an array named DEFAULT_EVENTS
which we will use later to map over and add the event listeners. Then we returned a ref object from the hook which will be used to attach the elements outside of which if clicked hook should trigger the handler
function.
The next step is to add the event listeners to the document. Whenever the click event or touch event occurs in the document, they will capture the event.
useEffect(() => {
const listener = (event) => {
// Main logic
};
DEFAULT_EVENTS.forEach((event) => document.addEventListener(event, listener));
return () => {
DEFAULT_EVENTS.forEach((event) => document.removeEventListener(event, listener));
};
}, [element, handler]);
Here we are adding the event listeners in the useEffect
hook by mapping over the DEFAULT_EVENTS
array and also removing the listeners when the hook unmounts. Every time the events are fired it will call the listener
function.
Now we will write the main logic in the listener
function.
const listener = (event) => {
// Do nothing if clicking ref's element or descendent elements
if (!element.current
|| element.current.contains(event.target)
|| (nodes && nodes.some((item) => item.contains(event.target)))) {
return;
}
handler(event);
};
Inside the listener
function we first check if the click event triggered on the specified element or not. If the event occurs on the specified element or from the nodes
array then the handler
function will not be called. If the condition is false then we will call the handler
function with event
as an argument.
Note: The
nodes
array should contain the elements only and not refs. usinguseState
is recommended for setting refs.
Sum up
import { useEffect, useRef } from "react";
const DEFAULT_EVENTS = ['mousedown', 'touchstart'];
export const useOnClickOutside = ({ handler, nodes }) => {
const element = useRef()
useEffect(() => {
const listener = (event) => {
// Do nothing if clicking ref's element or descendent elements
if (!element.current
|| element.current.contains(event.target)
|| (nodes && nodes.some((item) => item.contains(event.target)))) {
return;
}
handler(event);
};
DEFAULT_EVENTS.forEach((event) => document.addEventListener(event, listener));
return () => {
DEFAULT_EVENTS.forEach((event) => document.removeEventListener(event, listener));
};
}, [element, handler]);
return element
}
Live demo
If this blog helped you then do drop your favorite reaction(s) and let me know what you liked or any feedback you want me to work on.