Detect click outside React component
JavascriptDomReactjsJavascript Problem Overview
I'm looking for a way to detect if a click event happened outside of a component, as described in this article. jQuery closest() is used to see if the target from a click event has the dom element as one of its parents. If there is a match the click event belongs to one of the children and is thus not considered to be outside of the component.
So in my component I want to attach a click handler to window. When the handler fires I need to compare the target with the dom children of my component.
The click event contains properties like "path" which seems to hold the dom path that the event has travelled. I'm not sure what to compare or how to best traverse it, and I'm thinking someone must have already put that in a clever utility function... No?
Javascript Solutions
Solution 1 - Javascript
The following solution uses ES6 and follows best practices for binding as well as setting the ref through a method.
To see it in action:
Hooks Implementation:
import React, { useRef, useEffect } from "react";
/**
* Hook that alerts clicks outside of the passed ref
*/
function useOutsideAlerter(ref) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
/**
* Component that alerts if you click outside of it
*/
export default function OutsideAlerter(props) {
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef);
return <div ref={wrapperRef}>{props.children}</div>;
}
Class Implementation:
After 16.3
import React, { Component } from "react";
/**
* Component that alerts if you click outside of it
*/
export default class OutsideAlerter extends Component {
constructor(props) {
super(props);
this.wrapperRef = React.createRef();
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
/**
* Alert if clicked on outside of element
*/
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
alert("You clicked outside of me!");
}
}
render() {
return <div ref={this.wrapperRef}>{this.props.children}</div>;
}
}
Before 16.3
import React, { Component } from "react";
/**
* Component that alerts if you click outside of it
*/
export default class OutsideAlerter extends Component {
constructor(props) {
super(props);
this.setWrapperRef = this.setWrapperRef.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
/**
* Set the wrapper ref
*/
setWrapperRef(node) {
this.wrapperRef = node;
}
/**
* Alert if clicked on outside of element
*/
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
alert("You clicked outside of me!");
}
}
render() {
return <div ref={this.setWrapperRef}>{this.props.children}</div>;
}
}
Solution 2 - Javascript
2021 Update:
It has bee a while since I added this response, and since it still seems to garner some interest, I thought I would update it to a more current React version. On 2021, this is how I would write this component:
import React, { useState } from "react";
import "./DropDown.css";
export function DropDown({ options, callback }) {
const [selected, setSelected] = useState("");
const [expanded, setExpanded] = useState(false);
function expand() {
setExpanded(true);
}
function close() {
setExpanded(false);
}
function select(event) {
const value = event.target.textContent;
callback(value);
close();
setSelected(value);
}
return (
<div className="dropdown" tabIndex={0} onFocus={expand} onBlur={close} >
<div>{selected}</div>
{expanded ? (
<div className={"dropdown-options-list"}>
{options.map((O) => (
<div className={"dropdown-option"} onClick={select}>
{O}
</div>
))}
</div>
) : null}
</div>
);
}
Original Answer (2016):
Here is the solution that best worked for me without attaching events to the container:
Certain HTML elements can have what is known as "focus", for example input elements. Those elements will also respond to the blur event, when they lose that focus.
To give any element the capacity to have focus, just make sure its tabindex attribute is set to anything other than -1. In regular HTML that would be by setting the tabindex
attribute, but in React you have to use tabIndex
(note the capital I
).
You can also do it via JavaScript with element.setAttribute('tabindex',0)
This is what I was using it for, to make a custom DropDown menu.
var DropDownMenu = React.createClass({
getInitialState: function(){
return {
expanded: false
}
},
expand: function(){
this.setState({expanded: true});
},
collapse: function(){
this.setState({expanded: false});
},
render: function(){
if(this.state.expanded){
var dropdown = ...; //the dropdown content
} else {
var dropdown = undefined;
}
return (
<div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
<div className="currentValue" onClick={this.expand}>
{this.props.displayValue}
</div>
{dropdown}
</div>
);
}
});
Solution 3 - Javascript
I was stuck on the same issue. I am a bit late to the party here, but for me this is a really good solution. Hopefully it will be of help to someone else. You need to import findDOMNode
from react-dom
import ReactDOM from 'react-dom';
// ... ✂
componentDidMount() {
document.addEventListener('click', this.handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside, true);
}
handleClickOutside = event => {
const domNode = ReactDOM.findDOMNode(this);
if (!domNode || !domNode.contains(event.target)) {
this.setState({
visible: false
});
}
}
React Hooks Approach (16.8 +)
You can create a reusable hook called useComponentVisible
.
import { useState, useEffect, useRef } from 'react';
export default function useComponentVisible(initialIsVisible) {
const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
const ref = useRef(null);
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
setIsComponentVisible(false);
}
};
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, []);
return { ref, isComponentVisible, setIsComponentVisible };
}
Then in the component you wish to add the functionality to do the following:
const DropDown = () => {
const { ref, isComponentVisible } = useComponentVisible(true);
return (
<div ref={ref}>
{isComponentVisible && (<p>Dropdown Component</p>)}
</div>
);
}
Find a codesandbox example here.
Solution 4 - Javascript
After trying many methods here, I decided to use github.com/Pomax/react-onclickoutside because of how complete it is.
I installed the module via npm and imported it into my component:
import onClickOutside from 'react-onclickoutside'
Then, in my component class I defined the handleClickOutside
method:
handleClickOutside = () => {
console.log('onClickOutside() method called')
}
And when exporting my component I wrapped it in onClickOutside()
:
export default onClickOutside(NameOfComponent)
That's it.
Solution 5 - Javascript
Hook implementation based on Tanner Linsley's excellent talk at JSConf Hawaii 2020:
useOuterClick
API
const Client = () => {
const innerRef = useOuterClick(ev => {/*event handler code on outer click*/});
return <div ref={innerRef}> Inside </div>
};
Implementation
function useOuterClick(callback) {
const callbackRef = useRef(); // initialize mutable ref, which stores callback
const innerRef = useRef(); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current value
useEffect(() => { callbackRef.current = callback; });
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
function handleClick(e) {
if (innerRef.current && callbackRef.current &&
!innerRef.current.contains(e.target)
) callbackRef.current(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}
Here is a working example:
/*
Custom Hook
*/
function useOuterClick(callback) {
const innerRef = useRef();
const callbackRef = useRef();
// set current callback in ref, before second useEffect uses it
useEffect(() => { // useEffect wrapper to be safe for concurrent mode
callbackRef.current = callback;
});
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
// read most recent callback and innerRef dom node from refs
function handleClick(e) {
if (
innerRef.current &&
callbackRef.current &&
!innerRef.current.contains(e.target)
) {
callbackRef.current(e);
}
}
}, []); // no need for callback + innerRef dep
return innerRef; // return ref; client can omit `useRef`
}
/*
Usage
*/
const Client = () => {
const [counter, setCounter] = useState(0);
const innerRef = useOuterClick(e => {
// counter state is up-to-date, when handler is called
alert(`Clicked outside! Increment counter to ${counter + 1}`);
setCounter(c => c + 1);
});
return (
<div>
<p>Click outside!</p>
<div id="container" ref={innerRef}>
Inside, counter: {counter}
</div>
</div>
);
};
ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>
Key points
useOuterClick
makes use of mutable refs to provide leanClient
API- stable click listener for lifetime of containing component (
[]
deps) Client
can set callback without needing to memoize it byuseCallback
- callback body has access to the most recent props and state - no stale closure values
(Side note for iOS)
iOS in general treats only certain elements as clickable. To make outer clicks work, choose a different click listener than document
- nothing upwards including body
. E.g. add a listener on the React root div
and expand its height, like height: 100vh
, to catch all outside clicks. Source: quirksmode.org
Solution 6 - Javascript
None of the other answers here worked for me. I was trying to hide a popup on blur, but since the contents were absolutely positioned, the onBlur was firing even on the click of inner contents too.
Here is an approach that did work for me:
// Inside the component:
onBlur(event) {
// currentTarget refers to this component.
// relatedTarget refers to the element where the user clicked (or focused) which
// triggered this event.
// So in effect, this condition checks if the user clicked outside the component.
if (!event.currentTarget.contains(event.relatedTarget)) {
// do your thing.
}
},
Hope this helps.
Solution 7 - Javascript
[Update] Solution with React ^16.8 using Hooks
import React, { useEffect, useRef, useState } from 'react';
const SampleComponent = () => {
const [clickedOutside, setClickedOutside] = useState(false);
const myRef = useRef();
const handleClickOutside = e => {
if (!myRef.current.contains(e.target)) {
setClickedOutside(true);
}
};
const handleClickInside = () => setClickedOutside(false);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
});
return (
<button ref={myRef} onClick={handleClickInside}>
{clickedOutside ? 'Bye!' : 'Hello!'}
</button>
);
};
export default SampleComponent;
Solution with React ^16.3:
import React, { Component } from "react";
class SampleComponent extends Component {
state = {
clickedOutside: false
};
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
myRef = React.createRef();
handleClickOutside = e => {
if (!this.myRef.current.contains(e.target)) {
this.setState({ clickedOutside: true });
}
};
handleClickInside = () => this.setState({ clickedOutside: false });
render() {
return (
<button ref={this.myRef} onClick={this.handleClickInside}>
{this.state.clickedOutside ? "Bye!" : "Hello!"}
</button>
);
}
}
export default SampleComponent;
Solution 8 - Javascript
I found a solution thanks to Ben Alpert on discuss.reactjs.org. The suggested approach attaches a handler to the document but that turned out to be problematic. Clicking on one of the components in my tree resulted in a rerender which removed the clicked element on update. Because the rerender from React happens before the document body handler is called, the element was not detected as "inside" the tree.
The solution to this was to add the handler on the application root element.
main:
window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)
component:
import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
export default class ClickListener extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
onClickOutside: PropTypes.func.isRequired
}
componentDidMount () {
window.__myapp_container.addEventListener('click', this.handleDocumentClick)
}
componentWillUnmount () {
window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
}
/* using fat arrow to bind to instance */
handleDocumentClick = (evt) => {
const area = ReactDOM.findDOMNode(this.refs.area);
if (!area.contains(evt.target)) {
this.props.onClickOutside(evt)
}
}
render () {
return (
<div ref='area'>
{this.props.children}
</div>
)
}
}
Solution 9 - Javascript
The Ez way...
const componentRef = useRef();
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
function handleClick(e: any) {
if(componentRef && componentRef.current){
const ref: any = componentRef.current
if(!ref.contains(e.target)){
// put your action here
}
}
}
}, []);
then put the ref on your component
<div ref={componentRef as any}> My Component </div>
Solution 10 - Javascript
MUI has a small component to solve this problem: https://mui.com/base/react-click-away-listener/ that you can cherry-pick it. It weights below 1 kB gzipped, it supports mobile, IE 11, and portals.
Solution 11 - Javascript
Alternatively:
const onClickOutsideListener = () => {
alert("click outside")
document.removeEventListener("click", onClickOutsideListener)
}
...
return (
<div
onMouseLeave={() => {
document.addEventListener("click", onClickOutsideListener)
}}
>
...
</div>
Solution 12 - Javascript
For those who need absolute positioning, a simple option I opted for is to add a wrapper component that is styled to cover the whole page with a transparent background. Then you can add an onClick on this element to close your inside component.
<div style={{
position: 'fixed',
top: '0', right: '0', bottom: '0', left: '0',
zIndex: '1000',
}} onClick={() => handleOutsideClick()} >
<Content style={{position: 'absolute'}}/>
</div>
As it is right now if you add a click handler on content, the event will also be propagated to the upper div and therefore trigger the handlerOutsideClick. If this is not your desired behavior, simply stop the event progation on your handler.
<Content style={{position: 'absolute'}} onClick={e => {
e.stopPropagation();
desiredFunctionCall();
}}/>
`
Solution 13 - Javascript
I did this partly by following this and by following the React official docs on handling refs which requires react ^16.3. This is the only thing that worked for me after trying some of the other suggestions here...
class App extends Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentWillMount() {
document.addEventListener("mousedown", this.handleClick, false);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClick, false);
}
handleClick = e => {
/*Validating click is made inside a component*/
if ( this.inputRef.current === e.target ) {
return;
}
this.handleclickOutside();
};
handleClickOutside(){
/*code to handle what to do when clicked outside*/
}
render(){
return(
<div>
<span ref={this.inputRef} />
</div>
)
}
}
Solution 14 - Javascript
This already has many answers but they don't address e.stopPropagation()
and preventing clicking on react links outside of the element you wish to close.
Due to the fact that React has it's own artificial event handler you aren't able to use document as the base for event listeners. You need to e.stopPropagation()
before this as React uses document itself. If you use for example document.querySelector('body')
instead. You are able to prevent the click from the React link. Following is an example of how I implement click outside and close.
This uses ES6 and React 16.3.
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
isOpen: false,
};
this.insideContainer = React.createRef();
}
componentWillMount() {
document.querySelector('body').addEventListener("click", this.handleClick, false);
}
componentWillUnmount() {
document.querySelector('body').removeEventListener("click", this.handleClick, false);
}
handleClick(e) {
/* Check that we've clicked outside of the container and that it is open */
if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
e.preventDefault();
e.stopPropagation();
this.setState({
isOpen: false,
})
}
};
togggleOpenHandler(e) {
e.preventDefault();
this.setState({
isOpen: !this.state.isOpen,
})
}
render(){
return(
<div>
<span ref={this.insideContainer}>
<a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
</span>
<a href="/" onClick({/* clickHandler */})>
Will not trigger a click when inside is open.
</a>
</div>
);
}
}
export default App;
Solution 15 - Javascript
import { useClickAway } from "react-use";
useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
Solution 16 - Javascript
Here is my approach (demo - https://jsfiddle.net/agymay93/4/):
I've created special component called WatchClickOutside
and it can be used like (I assume JSX
syntax):
<WatchClickOutside onClickOutside={this.handleClose}>
<SomeDropdownEtc>
</WatchClickOutside>
Here is code of WatchClickOutside
component:
import React, { Component } from 'react';
export default class WatchClickOutside extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
document.body.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
// remember to remove all events to avoid memory leaks
document.body.removeEventListener('click', this.handleClick);
}
handleClick(event) {
const {container} = this.refs; // get container that we'll wait to be clicked outside
const {onClickOutside} = this.props; // get click outside callback
const {target} = event; // get direct click event target
// if there is no proper callback - no point of checking
if (typeof onClickOutside !== 'function') {
return;
}
// if target is container - container was not clicked outside
// if container contains clicked target - click was not outside of it
if (target !== container && !container.contains(target)) {
onClickOutside(event); // clicked outside - fire callback
}
}
render() {
return (
<div ref="container">
{this.props.children}
</div>
);
}
}
Solution 17 - Javascript
To extend on the accepted answer made by Ben Bud, if you are using styled-components, passing refs that way will give you an error such as "this.wrapperRef.contains is not a function".
The suggested fix, in the comments, to wrap the styled component with a div and pass the ref there, works. Having said that, in their docs they already explain the reason for this and the proper use of refs within styled-components:
> Passing a ref prop to a styled component will give you an instance of the StyledComponent wrapper, but not to the underlying DOM node. This is due to how refs work. It's not possible to call DOM methods, like focus, on our wrappers directly. To get a ref to the actual, wrapped DOM node, pass the callback to the innerRef prop instead.
Like so:
<StyledDiv innerRef={el => { this.el = el }} />
Then you can access it directly within the "handleClickOutside" function:
handleClickOutside = e => {
if (this.el && !this.el.contains(e.target)) {
console.log('clicked outside')
}
}
This also applies for the "onBlur" approach:
componentDidMount(){
this.el.focus()
}
blurHandler = () => {
console.log('clicked outside')
}
render(){
return(
<StyledDiv
onBlur={this.blurHandler}
tabIndex="0"
innerRef={el => { this.el = el }}
/>
)
}
Solution 18 - Javascript
This is my way of solving the problem
I return a boolean value from my custom hook, and when this value changes (true if the click was outside of the ref that I passed as an arg), this way i can catch this change with an useEffect hook, i hope it's clear for you.
Here's a live example: Live Example on codesandbox
import { useEffect, useRef, useState } from "react";
const useOutsideClick = (ref) => {
const [outsieClick, setOutsideClick] = useState(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (!ref.current.contains(e.target)) {
setOutsideClick(true);
} else {
setOutsideClick(false);
}
setOutsideClick(null);
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
return outsieClick;
};
export const App = () => {
const buttonRef = useRef(null);
const buttonClickedOutside = useOutsideClick(buttonRef);
useEffect(() => {
// if the the click was outside of the button
// do whatever you want
if (buttonClickedOutside) {
alert("hey you clicked outside of the button");
}
}, [buttonClickedOutside]);
return (
<div className="App">
<button ref={buttonRef}>click outside me</button>
</div>
);
}
Solution 19 - Javascript
I used this module (I have no association with the author)
npm install react-onclickout --save
const ClickOutHandler = require('react-onclickout');
class ExampleComponent extends React.Component {
onClickOut(e) {
if (hasClass(e.target, 'ignore-me')) return;
alert('user clicked outside of the component!');
}
render() {
return (
<ClickOutHandler onClickOut={this.onClickOut}>
<div>Click outside of me!</div>
</ClickOutHandler>
);
}
}
It did the job nicely.
Solution 20 - Javascript
Typescript with Hooks
Note: I'm using React version 16.3, with React.createRef. For other versions use the ref callback.
Dropdown component:
interface DropdownProps {
...
};
export const Dropdown: React.FC<DropdownProps> () {
const ref: React.RefObject<HTMLDivElement> = React.createRef();
const handleClickOutside = (event: MouseEvent) => {
if (ref && ref !== null) {
const cur = ref.current;
if (cur && !cur.contains(event.target as Node)) {
// close all dropdowns
}
}
}
useEffect(() => {
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
});
return (
<div ref={ref}>
...
</div>
);
}
Solution 21 - Javascript
So I faced a similar problem but in my case the selected answer here wasn't working because I had a button for the dropdown which is, well, a part of the document. So clicking the button also triggered the handleClickOutside
function. To stop that from triggering, I had to add a new ref
to the button and this !menuBtnRef.current.contains(e.target)
to the conditional. I'm leaving it here if someone is facing the same issue like me.
Here's how the component looks like now:
const Component = () => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const menuRef = useRef(null);
const menuBtnRef = useRef(null);
const handleDropdown = (e) => {
setIsDropdownOpen(!isDropdownOpen);
}
const handleClickOutside = (e) => {
if (menuRef.current && !menuRef.current.contains(e.target) && !menuBtnRef.current.contains(e.target)) {
setIsDropdownOpen(false);
}
}
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside, true);
return () => {
document.removeEventListener('mousedown', handleClickOutside, true);
};
}, []);
return (
<button ref={menuBtnRef} onClick={handleDropdown}></button>
<div ref={menuRef} className={`${isDropdownOpen ? styles.dropdownMenuOpen : ''}`}>
// ...dropdown items
</div>
)
}
Solution 22 - Javascript
My biggest concern with all of the other answers is having to filter click events from the root/parent down. I found the easiest way was to simply set a sibling element with position: fixed, a z-index 1 behind the dropdown and handle the click event on the fixed element inside the same component. Keeps everything centralized to a given component.
Example code
#HTML
<div className="parent">
<div className={`dropdown ${this.state.open ? open : ''}`}>
...content
</div>
<div className="outer-handler" onClick={() => this.setState({open: false})}>
</div>
</div>
#SASS
.dropdown {
display: none;
position: absolute;
top: 0px;
left: 0px;
z-index: 100;
&.open {
display: block;
}
}
.outer-handler {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
z-index: 99;
display: none;
&.open {
display: block;
}
}
Solution 23 - Javascript
componentWillMount(){
document.addEventListener('mousedown', this.handleClickOutside)
}
handleClickOutside(event) {
if(event.path[0].id !== 'your-button'){
this.setState({showWhatever: false})
}
}
Event path[0]
is the last item clicked
Solution 24 - Javascript
UseOnClickOutside Hook - React 16.8 +
Create a general useOnOutsideClick function
export const useOnOutsideClick = handleOutsideClick => {
const innerBorderRef = useRef();
const onClick = event => {
if (
innerBorderRef.current &&
!innerBorderRef.current.contains(event.target)
) {
handleOutsideClick();
}
};
useMountEffect(() => {
document.addEventListener("click", onClick, true);
return () => {
document.removeEventListener("click", onClick, true);
};
});
return { innerBorderRef };
};
const useMountEffect = fun => useEffect(fun, []);
Then use the hook in any functional component.
const OutsideClickDemo = ({ currentMode, changeContactAppMode }) => {
const [open, setOpen] = useState(false);
const { innerBorderRef } = useOnOutsideClick(() => setOpen(false));
return (
<div>
<button onClick={() => setOpen(true)}>open</button>
{open && (
<div ref={innerBorderRef}>
<SomeChild/>
</div>
)}
</div>
);
};
Partially inspired by @pau1fitzgerald answer.
Solution 25 - Javascript
In my DROPDOWN case the Ben Bud's solution worked well, but I had a separate toggle button with an onClick handler. So the outside clicking logic conflicted with the button onClick toggler. Here is how I solved it by passing the button's ref as well:
import React, { useRef, useEffect, useState } from "react";
/**
* Hook that triggers onClose when clicked outside of ref and buttonRef elements
*/
function useOutsideClicker(ref, buttonRef, onOutsideClick) {
useEffect(() => {
function handleClickOutside(event) {
/* clicked on the element itself */
if (ref.current && !ref.current.contains(event.target)) {
return;
}
/* clicked on the toggle button */
if (buttonRef.current && !buttonRef.current.contains(event.target)) {
return;
}
/* If it's something else, trigger onClose */
onOutsideClick();
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
/**
* Component that alerts if you click outside of it
*/
export default function DropdownMenu(props) {
const wrapperRef = useRef(null);
const buttonRef = useRef(null);
const [dropdownVisible, setDropdownVisible] = useState(false);
useOutsideClicker(wrapperRef, buttonRef, closeDropdown);
const toggleDropdown = () => setDropdownVisible(visible => !visible);
const closeDropdown = () => setDropdownVisible(false);
return (
<div>
<button onClick={toggleDropdown} ref={buttonRef}>Dropdown Toggler</button>
{dropdownVisible && <div ref={wrapperRef}>{props.children}</div>}
</div>
);
}
Solution 26 - Javascript
Typescript + simplified version of @ford04's proposal:
useOuterClick
API
const Client = () => {
const ref = useOuterClick<HTMLDivElement>(e => { /* Custom-event-handler */ });
return <div ref={ref}> Inside </div>
};
Implementation
export default function useOuterClick<T extends HTMLElement>(callback: Function) {
const callbackRef = useRef<Function>(); // initialize mutable ref, which stores callback
const innerRef = useRef<T>(null); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current value
useEffect(() => { callbackRef.current = callback; });
useEffect(() => {
document.addEventListener("click", _onClick);
return () => document.removeEventListener("click", _onClick);
function _onClick(e: any): void {
const clickedOutside = !(innerRef.current?.contains(e.target));
if (clickedOutside)
callbackRef.current?.(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}
Solution 27 - Javascript
Simply with ClickAwayListener from mui (material-ui):
<ClickAwayListener onClickAway={handleClickAway}>
{children}
<ClickAwayListener >
for more info you can check:https://mui.com/base/react-click-away-listener/
Solution 28 - Javascript
An example with Strategy
I like the provided solutions that use to do the same thing by creating a wrapper around the component.
Since this is more of a behavior I thought of Strategy and came up with the following.
I'm new with React and I need a bit of help in order to save some boilerplate in the use cases
Please review and tell me what you think.
ClickOutsideBehavior
import ReactDOM from 'react-dom';
export default class ClickOutsideBehavior {
constructor({component, appContainer, onClickOutside}) {
// Can I extend the passed component's lifecycle events from here?
this.component = component;
this.appContainer = appContainer;
this.onClickOutside = onClickOutside;
}
enable() {
this.appContainer.addEventListener('click', this.handleDocumentClick);
}
disable() {
this.appContainer.removeEventListener('click', this.handleDocumentClick);
}
handleDocumentClick = (event) => {
const area = ReactDOM.findDOMNode(this.component);
if (!area.contains(event.target)) {
this.onClickOutside(event)
}
}
}
Sample Usage
import React, {Component} from 'react';
import {APP_CONTAINER} from '../const';
import ClickOutsideBehavior from '../ClickOutsideBehavior';
export default class AddCardControl extends Component {
constructor() {
super();
this.state = {
toggledOn: false,
text: ''
};
this.clickOutsideStrategy = new ClickOutsideBehavior({
component: this,
appContainer: APP_CONTAINER,
onClickOutside: () => this.toggleState(false)
});
}
componentDidMount () {
this.setState({toggledOn: !!this.props.toggledOn});
this.clickOutsideStrategy.enable();
}
componentWillUnmount () {
this.clickOutsideStrategy.disable();
}
toggleState(isOn) {
this.setState({toggledOn: isOn});
}
render() {...}
}
Notes
I thought of storing the passed component
lifecycle hooks and override them with methods simillar to this:
const baseDidMount = component.componentDidMount;
component.componentDidMount = () => {
this.enable();
baseDidMount.call(component)
}
component
is the component passed to the constructor of ClickOutsideBehavior
.
This will remove the enable/disable boilerplate from the user of this behavior but it doesn't look very nice though
Solution 29 - Javascript
I know this is an old question, but I keep coming across this and I had a lot of trouble figuring this out in a simple format. So if this would make anyones life a bit easier, use OutsideClickHandler by airbnb. It is a the simplest plugin to accomplish this task without writing your own code.
Example:
hideresults(){
this.setState({show:false})
}
render(){
return(
<div><div onClick={() => this.setState({show:true})}>SHOW</div> {(this.state.show)? <OutsideClickHandler onOutsideClick={() =>
{this.hideresults()}} > <div className="insideclick"></div> </OutsideClickHandler> :null}</div>
)
}
Solution 30 - Javascript
Alternatively to .contains, you can use the .closest method. When you want to check if a click was outside of the element with id="apple" then i can use:
const isOutside = !e.target.closest("#apple");
This checks if any element in the tree above the clicked one has an id of "apple". We have to negate the result!
Solution 31 - Javascript
I like the @Ben Bud's answer but when there are visually nested elements, contains(event.target)
not works as expected.
So sometimes it's better to calculate the clicked point is visually inside of the element or not.
Here is my React Hook code for the situation.
import { useEffect } from 'react'
export function useOnClickRectOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
const targetEl = ref.current
if (targetEl) {
const clickedX = event.clientX
const clickedY = event.clientY
const rect = targetEl.getBoundingClientRect()
const targetElTop = rect.top
const targetElBottom = rect.top + rect.height
const targetElLeft = rect.left
const targetElRight = rect.left + rect.width
if (
// check X Coordinate
targetElLeft < clickedX &&
clickedX < targetElRight &&
// check Y Coordinate
targetElTop < clickedY &&
clickedY < targetElBottom
) {
return
}
// trigger event when the clickedX,Y is outside of the targetEl
handler(event)
}
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, handler])
}
Solution 32 - Javascript
NON INTRUSTIVE WAY NO NEED TO ADD ANOTHER DIV EL.
Note: React may say findDomNode isDeprecated but till now I have not faced any issue with it
@exceptions: class to ignore if clicked on it
@idException: id to ignore if clicked on it
import React from "react"
import ReactDOM from "react-dom"
type Func1<T1, R> = (a1: T1) => R
export function closest(
el: Element,
fn: (el: Element) => boolean
): Element | undefined {
let el_: Element | null = el;
while (el_) {
if (fn(el_)) {
return el_;
}
el_ = el_.parentElement;
}
}
let instances: ClickOutside[] = []
type Props = {
idException?: string,
exceptions?: (string | Func1<MouseEvent, boolean>)[]
handleClickOutside: Func1<MouseEvent, void>
}
export default class ClickOutside extends React.Component<Props> {
static defaultProps = {
exceptions: []
};
componentDidMount() {
if (instances.length === 0) {
document.addEventListener("mousedown", this.handleAll, true)
window.parent.document.addEventListener(
"mousedown",
this.handleAll,
true
)
}
instances.push(this)
}
componentWillUnmount() {
instances.splice(instances.indexOf(this), 1)
if (instances.length === 0) {
document.removeEventListener("mousedown", this.handleAll, true)
window.parent.document.removeEventListener(
"mousedown",
this.handleAll,
true
)
}
}
handleAll = (e: MouseEvent) => {
const target: HTMLElement = e.target as HTMLElement
if (!target) return
instances.forEach(instance => {
const { exceptions, handleClickOutside: onClickOutside, idException } = instance.props as Required<Props>
let exceptionsCount = 0
if (exceptions.length > 0) {
const { functionExceptions, stringExceptions } = exceptions.reduce(
(acc, exception) => {
switch (typeof exception) {
case "function":
acc.functionExceptions.push(exception)
break
case "string":
acc.stringExceptions.push(exception)
break
}
return acc
},
{ functionExceptions: [] as Func1<MouseEvent, boolean>[], stringExceptions: [] as string[] }
)
if (functionExceptions.length > 0) {
exceptionsCount += functionExceptions.filter(
exception => exception(e) === true
).length
}
if (exceptionsCount === 0 && stringExceptions.length > 0) {
const el = closest(target, (e) => stringExceptions.some(ex => e.classList.contains(ex)))
if (el) {
exceptionsCount++
}
}
}
if (idException) {
const target = e.target as HTMLDivElement
if (document.getElementById(idException)!.contains(target)) {
exceptionsCount++
}
}
if (exceptionsCount === 0) {
// eslint-disable-next-line react/no-find-dom-node
const node = ReactDOM.findDOMNode(instance)
if (node && !node.contains(target)) {
onClickOutside(e)
}
}
})
};
render() {
return React.Children.only(this.props.children)
}
}
Usage
<ClickOutside {...{ handleClickOutside: () => { alert('Clicked Outside') } }}>
<div >
<div>Breathe</div>
</div>
</ClickOutside>
Solution 33 - Javascript
with typescript
function Tooltip(): ReactElement {
const [show, setShow] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent): void {
if (ref.current && !ref.current.contains(event.target as Node)) {
setShow(false);
}
}
// Bind the event listener
document.addEventListener('mousedown', handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener('mousedown', handleClickOutside);
};
});
return (
<div ref={ref}></div>
)
}
Solution 34 - Javascript
I found this from the article below:
render() { return (
Here is a great article about this issue: "Handle clicks outside of React components" https://larsgraubner.com/handle-outside-clicks-react/
Solution 35 - Javascript
you can you a simple way to solve your problem , i show you :
....
const [dropDwonStatus , setDropDownStatus] = useState(false)
const openCloseDropDown = () =>{
setDropDownStatus(prev => !prev)
}
const closeDropDown = ()=> {
if(dropDwonStatus){
setDropDownStatus(false)
}
}
.
.
.
<parent onClick={closeDropDown}>
<child onClick={openCloseDropDown} />
</parent>
this works for me , good luck ;)
Solution 36 - Javascript
https://stackoverflow.com/a/42234988/9536897 solution doesn't work on mobile phones.
You can try:
// returns true if the element or one of its parents has the class classname
hasSomeParentTheClass(element, classname) {
if(element.target)
element=element.target;
if (element.className&& element.className.split(" ").indexOf(classname) >= 0) return true;
return (
element.parentNode &&
this.hasSomeParentTheClass(element.parentNode, classname)
);
}
componentDidMount() {
const fthis = this;
$(window).click(function (element) {
if (!fthis.hasSomeParentTheClass(element, "myClass"))
fthis.setState({ pharmacyFocus: null });
});
}
- On the view, gave className to your specific element.
Solution 37 - Javascript
I had a case when I needed to insert children into the modal conditionally. Something like this, bellow.
const [view, setView] = useState(VIEWS.SomeView)
return (
<Modal onClose={onClose}>
{VIEWS.Result === view ? (
<Result onDeny={() => setView(VIEWS.Details)} />
) : VIEWS.Details === view ? (
<Details onDeny={() => setView(VIEWS.Result) /> />
) : null}
</Modal>
)
So !parent.contains(event.target)
doesn't work here, because once you detach children, parent (modal) doesn't contain event.target
anymore.
The solution I had (which works so far and have no any issue) is to write something like this:
const listener = (event: MouseEvent) => {
if (parentNodeRef && !event.path.includes(parentNodeRef)) callback()
}
If parent contained element from already detached tree, it wouldn't fire callback.
EDIT:
event.path
is new and doesn't exit in all browsers yet. Use compoesedPath
instead.
Solution 38 - Javascript
I had a similar use case where I had to develop a custom dropdown menu. it should close automatically when the user clicks outside. here is the recent React Hooks implementation-
import { useEffect, useRef, useState } from "react";
export const App = () => {
const ref = useRef();
const [isMenuOpen, setIsMenuOpen] = useState(false);
useEffect(() => {
const checkIfClickedOutside = (e) => {
// If the menu is open and the clicked target is not within the menu,
// then close the menu
if (isMenuOpen && ref.current && !ref.current.contains(e.target)) {
setIsMenuOpen(false);
}
};
document.addEventListener("mousedown", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("mousedown", checkIfClickedOutside);
};
}, [isMenuOpen]);
return (
<div className="wrapper" ref={ref}>
<button
className="button"
onClick={() => setIsMenuOpen((oldState) => !oldState)}
>
Click Me
</button>
{isMenuOpen && (
<ul className="list">
<li className="list-item">dropdown option 1</li>
<li className="list-item">dropdown option 2</li>
<li className="list-item">dropdown option 3</li>
<li className="list-item">dropdown option 4</li>
</ul>
)}
</div>
);
}
Solution 39 - Javascript
Add an onClick
handler to your top level container and increment a state value whenever the user clicks within. Pass that value to the relevant component and whenever the value changes you can do work.
In this case we're calling this.closeDropdown()
whenever the clickCount
value changes.
The incrementClickCount
method fires within the .app
container but not the .dropdown
because we use event.stopPropagation()
to prevent event bubbling.
Your code may end up looking something like this:
class App extends Component {
constructor(props) {
super(props);
this.state = {
clickCount: 0
};
}
incrementClickCount = () => {
this.setState({
clickCount: this.state.clickCount + 1
});
}
render() {
return (
<div className="app" onClick={this.incrementClickCount}>
<Dropdown clickCount={this.state.clickCount}/>
</div>
);
}
}
class Dropdown extends Component {
constructor(props) {
super(props);
this.state = {
open: false
};
}
componentDidUpdate(prevProps) {
if (this.props.clickCount !== prevProps.clickCount) {
this.closeDropdown();
}
}
toggleDropdown = event => {
event.stopPropagation();
return (this.state.open) ? this.closeDropdown() : this.openDropdown();
}
render() {
return (
<div className="dropdown" onClick={this.toggleDropdown}>
...
</div>
);
}
}
Solution 40 - Javascript
To make the 'focus' solution work for dropdown with event listeners you can add them with onMouseDown event instead of onClick. That way the event will fire and after that the popup will close like so:
<TogglePopupButton
onClick = { this.toggleDropup }
tabIndex = '0'
onBlur = { this.closeDropup }
/>
{ this.state.isOpenedDropup &&
<ul className = { dropupList }>
{ this.props.listItems.map((item, i) => (
<li
key = { i }
onMouseDown = { item.eventHandler }
>
{ item.itemName}
</li>
))}
</ul>
}
Solution 41 - Javascript
import ReactDOM from 'react-dom' ;
class SomeComponent {
constructor(props) {
// First, add this to your constructor
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentWillMount() {
document.addEventListener('mousedown', this.handleClickOutside, false);
}
// Unbind event on unmount to prevent leaks
componentWillUnmount() {
window.removeEventListener('mousedown', this.handleClickOutside, false);
}
handleClickOutside(event) {
if(!ReactDOM.findDOMNode(this).contains(event.path[0])){
console.log("OUTSIDE");
}
}
}
Solution 42 - Javascript
I made a solution for all occasions.
You should use a High Order Component to wrap the component that you would like to listen for clicks outside it.
This component example has only one prop: "onClickedOutside" that receives a function.
ClickedOutside.js
import React, { Component } from "react";
export default class ClickedOutside extends Component {
componentDidMount() {
document.addEventListener("mousedown", this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener("mousedown", this.handleClickOutside);
}
handleClickOutside = event => {
// IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
// A props callback for the ClikedClickedOutside
this.props.onClickedOutside();
}
};
render() {
// In this piece of code I'm trying to get to the first not functional component
// Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
let firstNotFunctionalComponent = this.props.children;
while (typeof firstNotFunctionalComponent.type === "function") {
firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
}
// Here I'm cloning the element because I have to pass a new prop, the "reference"
const children = React.cloneElement(firstNotFunctionalComponent, {
ref: node => {
this.wrapperRef = node;
},
// Keeping all the old props with the new element
...firstNotFunctionalComponent.props
});
return <React.Fragment>{children}</React.Fragment>;
}
}
Solution 43 - Javascript
Non of the above answers worked for me so here is what I did eventually:
import React, { Component } from 'react';
/**
* Component that alerts if you click outside of it
*/
export default class OutsideAlerter extends Component {
constructor(props) {
super(props);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
}
/**
* Alert if clicked on outside of element
*/
handleClickOutside(event) {
if (!event.path || !event.path.filter(item => item.className=='classOfAComponent').length) {
alert('You clicked outside of me!');
}
}
render() {
return <div>{this.props.children}</div>;
}
}
OutsideAlerter.propTypes = {
children: PropTypes.element.isRequired,
};
Solution 44 - Javascript
If you want to use a tiny component (466 Byte gzipped) that already exists for this functionality then you can check out this library react-outclick.
The good thing about the library is that it also lets you detect clicks outside of a component and inside of another. It also supports detecting other types of events.
Solution 45 - Javascript
Bit late to the party, but I was having issues getting any of these to work with a React.Select dropdown as the option clicked would no longer be contained within the parent I was looking to click out of by the time onClick was fired.
I got round this issue by using:
componentDidMount() {
document.addEventListener('mousedown', this.onClick );
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.onClick );
}
onClick = (event) => {
if(!event.path.includes(this.detectOutsideClicksDiv)) {
// Do stuff here
}
}
Solution 46 - Javascript
This is the method that suits me the best to make a dropdown menu:
handleClick = () => {
document.getElementById("myDrop").classList.toggle("showing");
}
render() {
return (
<div className="courses">
<div class="dropdownBody">
<button onClick={this.handleClick} onBlur={this.handleClick} class="dropbtn">Dropdown</button>
<div id="myDrop" class="dropdown-content">
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</div>
</div>
</div>
)
}
Solution 47 - Javascript
You could just install a double click handler on the body and another one on this element. In the handler of this element just return false to prevent the event from propagating. So when a double click happens if it is on the element it will be caught and will not propagate to the handler on the body. Otherwise it will be caught by the handler on the body.
Update: if you really do not want to prevent event propagation, you just need to use closest to check whether the click happened on your element or one of his children:
<html>
<head>
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script>
$(document).on('click', function(event) {
if (!$(event.target).closest('#div3').length) {
alert("outside");
}
});
</script>
</head>
<body>
<div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
<div style="background-color:red;width:100px;height:100px;" id="div2"></div>
<div style="background-color:green;width:100px;height:100px;" id="div3"></div>
<div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
<div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
</body>
</html>
Update: without jQuery:
<html>
<head>
<script>
function findClosest (element, fn) {
if (!element) return undefined;
return fn(element) ? element : findClosest(element.parentElement, fn);
}
document.addEventListener("click", function(event) {
var target = findClosest(event.target, function(el) {
return el.id == 'div3'
});
if (!target) {
alert("outside");
}
}, false);
</script>
</head>
<body>
<div style="background-color:blue;width:100px;height:100px;" id="div1"></div>
<div style="background-color:red;width:100px;height:100px;" id="div2"></div>
<div style="background-color:green;width:100px;height:100px;" id="div3">
<div style="background-color:pink;width:50px;height:50px;" id="div6"></div>
</div>
<div style="background-color:yellow;width:100px;height:100px;" id="div4"></div>
<div style="background-color:grey;width:100px;height:100px;" id="div5"></div>
</body>
</html>