Detecting Clicks Outside of a Component in React
It can be very useful to detect clicks outside of a specific component in your React application. For example, when you have a modal or a dropdown component, you might want to close them automatically when user clicks somewhere else on the page.
In this article, you will learn how detect if user has clicked outside of a component in React. Furthermore, you will see how to:
- create a reusable hook for detecting clicks outside of any component
- use an existing hook from react-use package that detects outside clicks
Detecting Clicks Outside of a Component
To demonstrate how to detect clicks outside of a component in React, I will use a basic modal component.
You can see the complete code here on CodeSandBox, otherwise let’s create it step by step.
Here’s an application that conditionally renders a modal:
export default function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<p>Page Content</p>
<button onClick={(event) => setShowModal(true)}>Open modal</button>
{showModal && <Modal onClose={() => setShowModal(false)} />}
</div>
);
}
- State variable
showModal
controls whether a modal is shown or not - Initially state is set to
false
so theModal
component is not rendered - If the button is pressed, state is set to
true
and theModal
is rendered - The
Modal
accepts a callback function via itsonClose
prop - When the
Modal
should be closed, you can call theonClose
callback function from theModal
and hide it
And here is the Modal
component itself:
export default function Modal({ onClose }) {
return (
<div>
<p>Modal content</p>
<button onClick={onClose}>Close modal</button>
</div>
);
}
For now, it can be closed only by pressing the close button.
Now, let’s add code to close the modal when clicking outside of it.
First, you need to listen to any mouse click. For that, you can add an event listener to the global document
object.
import { useEffect } from "react";
export default function Modal({ onClose }: { onClose: () => void }) {
useEffect(() => {
const handleClick = (event: MouseEvent) => {
// Handle the click event here
};
document.addEventListener("click", handleClick, true);
return () => {
document.removeEventListener("click", handleClick, true);
};
}, [onClose]);
return (
<div>
<p>Modal content</p>
<button onClick={onClose}>Close modal</button>
</div>
);
}
- You can use
useEffect
hook to register and deregister event listeners on thedocument
object - It is important to remove the event listener, otherwise
document
will keep listening to click events event afterModal
is closed and not rendered
Before you can handle the click event, you will need a reference to the modal DOM element. You need it so that you can tell if you have clicked on the modal itself. If that’s not the case, you have clicked outside.
import { useEffect, useRef } from "react";
export default function Modal({ onClose }: { onClose: () => void }) {
const modalRef = useRef<HTMLDivElement>(null);
// ✂️ useEffect(() => { /* ... */ });
return (
<div ref={modalRef}>
<p>Modal content</p>
<button onClick={onClose}>Close modal</button>
</div>
);
}
With those things in place, you can check if the click has targeted your modal. If it has, you ignore the click, otherwise the user has clicked outside of the modal and you can close it.
import { useEffect, useRef } from "react";
export default function Modal({ onClose }: { onClose: () => void }) {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClick = (event: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
onClose();
}
};
document.addEventListener("click", handleClick, true);
return () => {
document.removeEventListener("click", handleClick, true);
};
}, [modalRef, onClose]);
return (
<div ref={modalRef}>
<p>Modal content</p>
<button onClick={onClose}>Close modal</button>
</div>
);
}
Here’s what’s happening:
- You create a reference that will hold the entire DOM node of your modal, including its content
- You register an event listener that reacts to user clicks by calling
handleClick
- With
handleClick
, you:- Check if
modalRef
does not contain its initial value ofnull
- Check if the DOM node held by
modalRef
does not contain the same element that was clicked viaevent.target
- Check if
- You call the
onClose
function ifevent.target
contains element that is not found inmodalRef
A natural next step and big improvement you can do is to make this functionality reusable by moving it into a hook so that you can use it for other components.
Creating a Hook to Detect Clicks Outside Any Component
Now that you have seen how to automatically close a modal component when clicking outside of it, it is time to learn how to turn that functionality into a reusable hook. This way you can use it for any component you want, such as tooltips, dropdown menus or sidebars.
Here are the steps to create a reusable hook that detects clicks outside of a component in React.
-
Create a file for your hook like
src/hooks/useClickOutside.ts
and add the following codeimport { useEffect, useRef } from "react"; const useClickOutside = (callback: Function) => { const ref = useRef<any>(null); useEffect(() => { const handleClick = (event: MouseEvent) => { if (!ref.current?.contains(event.target as Node)) { callback(); } }; document.addEventListener("click", handleClick, true); return () => { document.removeEventListener("click", handleClick, true); }; }, [ref, callback]); return ref; }; export default useClickOutside;
This hook creates a reference and returns it. You will have to attach that reference to the HTML element which you want to detect clicks outside of.
-
Use the hook in your component by passing it a function you want to be called when a click outside of the referenced element is made
import { useState } from "react"; import useClickOutside from "../hooks/useClickOutside"; export default function Dropdown() { const [isOpen, setIsOpen] = useState(false); function handleClickOutside() { setIsOpen(false); } const ref = useClickOutside(handleClickOutside); return ( <div ref={ref} className="dropdown"> <button onClick={() => setIsOpen((value) => !value)}> Open dropdown </button> {isOpen && ( <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> )} </div> ); }
In this example, a dropdown menu can be opened or closed by pressing a button. It is also closed when clicking outside of the menu (
div
element) with the help ofuseClickOutside
hook that callshandleClickOutside
function.
Now that you have created the hook, you can attach its returned reference to any element you wish.
Using an Existing Solution
In the previous section you wrote a custom hook to detect clicks outside of any component. If you want to save yourself the work, you can use a ready-made solution from react-use package.
Here are the steps how to detect clicks outside of a component using react-use.
-
Install react-use library by running the install command from a command line from the root of your project
npm install react-use
-
Import and use
useClickAway
hook fromreact-use
import { useRef, useState } from "react"; import { useClickAway } from "react-use"; export default function Sidebar() { const [isOpen, setIsOpen] = useState(false); const ref = useRef(null); function handleClickOutside() { setIsOpen(false); } useClickAway(ref, handleClickOutside); return ( <> <button onClick={() => setIsOpen(true)}>Open sidebar</button> {isOpen && ( <div ref={ref}> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> </div> )} </> ); }
You have to create a reference using
useRef
hook and pass it touseClickAway
hook along with a function that you want to be called when a click outside of the referenced element is made.
Summary
In this article your learned how to detect clicks outside of a given component in React. You then extracted that functionality into its own custom hook, so that it can be reused for any component. You also saw how to achieve the same goal by using an existing solution from react-use.
If you want to see the complete code with examples of a modal, dropdown and a sidebar components, head to this CodeSandBox project.