Detect A Click Outside An Element Using React Hooks

Tony Pettigrew

Tony Pettigrew / October 04, 2022

4 min read––– views

Table of Contents:

Today we will be creating a hook to detect if a click happens outside of the component that the hook is called in. The use case for this, most of the time, is dynamically visible components such as modals, drawers and menus. If you get lost along the way, here is the example repo.

Planning It Out

As always, the first step is to create a plan and think about how to accomplish detecting a click outside of an element the React way:

  • Since we will need to detect a DOM element, immediately, I know that we will need to use a reference (or ref) to the component.
  • Since this click can occur anywhere, we will need to listen for it on the document body.
  • To make the hook work on mobile, we'll check for both mouse and touch events.
  • The component will need to be mounted before we can add event listeners so we'll need to utilize the useEffect hook to run our setup code.
  • We'll also need to pass in a handler callback which will be called when our click outside condition is met.
  • When the component unmounts we'll need to remove the event listers.

Writing The Hook

Now that we have an outline of what needs to happen, let's start writing the hook. We'll begin with the args.

Ref and Callback Args

The hook will need access to the element reference and function to call when the click outside condition is met.

import { RefObject } from "react";

type Event = MouseEvent | TouchEvent;

export const useOnClickOutside<T extends HTMLElement = HTMLElement> = (
  ref: RefObject<T>,
  callback: (event: Event) => void,
) => {

}

Setup Event Listeners

Now that we have access to the DOM element and callback function, we need to set up the event listeners as soon as the element mounts.

import { RefObject, useEffect } from "react";

type Event = MouseEvent | TouchEvent;

export const useOnClickOutside<T extends HTMLElement = HTMLElement> = (
  ref: RefObject<T>,
  callback: (event?: Event) => void,
) => {

  // We'll address the logic of the event handler next
  const listener = (event: Event) => {}

  useEffect(() => {
    // Add event listeners on mount
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);

    // Remove event listeners on unmount
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  })

}

Event Handler

The logic for the event handler (function called listener) will check the event parameter of listener to see if it contains our DOM element ref, if so, we return from the function. If it does not, we will call the callback function.

import { RefObject, useEffect } from "react";

type Event = MouseEvent | TouchEvent;

export const useOnClickOutside = <T extends HTMLElement = HTMLElement> (
  ref: RefObject<T>,
  callback: (event: Event) => void,
) => {

  const listener = (event: Event) => {
    const el = ref?.current;

    if (!el || el.contains(event?.target as Node)) {
      return;
    }

    callback(event);
  }

  useEffect(() => {
    // Add event listeners on mount
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);

    // Remove event listeners on unmount
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  })
}

How To Use

To use the hook you will also need to use React's useRef hook to capture the DOM element and pass it to our hook.

import { useRef } from "react";

import { useOnClickOutside } from "../hooks/useOnClickOutside";

export const ClickOutside = () => {
  const ref = useRef(null);

  // Will be called when the click outside condition is met.
  const onClickOutside = () => {
    console.log("Click Outside!");
  };

  useOnClickOutside(ref, onClickOutside);

  return (
    <div
      ref={ref}
      style={{
        width: 200,
        height: 200,
        background: "blue",
      }}
    />
  );
};

Usually the callback will either close the modal, drawer or menu. Please feel free to use this in your projects!