How to create a reusable useOnClickOutside hook in react?

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

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:-

  1. handler - A callback function to be executed when the event occurs.
  2. 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. using useState 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.