876

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 the 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 traveled. 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?

Yilmaz
  • 35,338
  • 10
  • 157
  • 202
Thijs Koerselman
  • 21,680
  • 22
  • 74
  • 108
  • Could you attach the click handler to the parent rather than the window? – J. Mark Stevens Sep 13 '15 at 18:47
  • If you attach a click handler to the parent you know when that element or one of their children is clicked, but I need to detect all *other* places that are clicked, so the handler needs to be attached to the window. – Thijs Koerselman Sep 13 '15 at 18:50
  • I looked at the article after the previous response. How about setting a clickState in the top component and passing click actions from the kids. Then you would check the props in the kids to manage open close state. – J. Mark Stevens Sep 13 '15 at 19:01
  • The top component would be my app. But the listening component is several levels deep and has no strict position in the dom. I can't possibly add click handlers to all components in my app just because one of them is interested to know if you clicked somewhere outside of it. Other components should not be aware of this logic because that would create terrible dependencies and boilerplate code. – Thijs Koerselman Sep 13 '15 at 19:14
  • If you are using flux you could have a store for the top component and another for the listening component. Then use actions and triggers to pass the state around. That would mean a click listener on the top component/window as well as the embedded component. – J. Mark Stevens Sep 13 '15 at 19:30
  • 20
    I would like to recommend you a very nice lib. created by AirBnb: https://github.com/airbnb/react-outside-click-handler – Osoian Marcel Apr 05 '19 at 13:35
  • How to dismiss dropdowns when pressed on background with React.js - https://sandny.com/2021/07/04/dismiss-dropdowns-when-pressed-on-background-with-react/ – ricky Jul 05 '21 at 12:55
  • If anyone wants to learn how to implement a "click outside detection"-hook in React, check out this tutorial: https://www.robinwieruch.de/react-hook-detect-click-outside-component/ It makes use of event capturing (instead of its more popular sibling event bubbling). – Robin Wieruch Apr 22 '22 at 09:22

56 Answers56

1502

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>;
  }
}

Ben Bud
  • 16,150
  • 2
  • 17
  • 13
  • 3
    How do you test this though? How do you send a valid event.target to the handleClickOutside function? – csilk Jul 12 '17 at 00:57
  • 3
    In your test you could render the adjacent to a
    . Simulate a click on the
    and listen for the alert method being called by using something like jest mocks or sinon spies
    – Ben Bud Jul 12 '17 at 16:01
  • 8
    How can you use React's synthetic events, instead of the `document.addEventListener` here? – Ralph David Abernathy Jul 26 '17 at 10:13
  • 1. You forgot `context` as second argument to constructor. 2. There's no more need in () around JSX. – polkovnikov.ph Sep 01 '17 at 07:50
  • @polkovnikov.ph: 1. `context` is not being used in this case and is not necessary to pass through. The only reason we pass through props is based off a recommendation from the react documentation: https://facebook.github.io/react/docs/state-and-lifecycle.html#adding-local-state-to-a-class. 2. Not sure what you mean by this. The jsx is inside of a return statement. We are using `()` so that we can have it start on it's own line for readability. – Ben Bud Sep 03 '17 at 21:04
  • @BenBud 1. Since when it is not necessary to pass through? It won't be available on inner nodes. Documentation is often outdated. 2. `return
    – polkovnikov.ph Sep 04 '17 at 06:55
  • 12
    @polkovnikov.ph 1. `context` is only necessary as an argument in constructor if it is used. It is not being used in this case. The reason the react team recommends to have `props` as an argument in the constructor is because use of `this.props` before calling `super(props)` will be undefined and can lead to errors. `context` is still available on inner nodes, that is the whole purpose of `context`. This way you don't have to pass it down from component to component like you do with props. 2. This is just a stylistic preference, and does not warrant debating over in this case. – Ben Bud Sep 08 '17 at 03:15
  • 2
    Something to note is the _mousedown_ event isn't triggered when using the keyboard to focus (tab) and activate (enter) a button or link outside of a component. The _click_ event however will also include keyboard use. So adding the listener to the _click_ event may be preferable in some situations. – Simon Watson Sep 22 '17 at 12:05
  • 1
    How do you get around the problem of creating multiple instances of an `OutsideAlerter` attaching multiple events to the document? – andy mccullough Oct 17 '17 at 10:57
  • @andymccullough I don't think you can get around of this - all those instances ARE different nodes after all, so you have to bind event listener to component instance when attaching it to document, and bind always creates new instance of function object. I can't think of any workaround that's also not increasing complexity significantly – Dr.Strangelove Dec 29 '17 at 10:03
  • This is a bit outdated. React doesn't have defined "PropTypes" (React.PropTypes) anymore, use Fragment instead of `div`, avoid using the bind, and the `ref` should receive a function – Broda Noel Mar 20 '18 at 17:02
  • Thanks @BrodaNoel! Good point on using `Fragment` and no longer using `React.PropTypes`. In regards to the other two points: 1) you should avoid using bind in render, however, doing it in the constructor will only call it once the instance is created and not on each rerender. 2) The ref is receiving a function in the current implementation. – Ben Bud Mar 22 '18 at 14:39
  • Sorry, you also can't pass a ref to ``. `key` is the only attribute that can be passed to Fragment so we'll still have to use a div here. – Ben Bud Mar 22 '18 at 14:56
  • @BenBud true. I forgot about that. Good catch. – Broda Noel Mar 22 '18 at 18:34
  • @BenBud I get "this.wrapperRef.contains is not a function" when clicking anywhere in the screen. Any ideas what might be wrong ? – SpiXel Mar 28 '18 at 06:17
  • Does this still work in React 16+ ?` ` so it gets triggered even if this button is clicked.... – Kunok Apr 12 '18 at 00:02
  • @Kunok, yes it will still work in React 16+. You can take a look at the code sandbox url I added to the answer to see it in action. – Ben Bud Apr 12 '18 at 15:15
  • @SpiXel seems like `wrapperRef` is not set yet when you are calling the `contains` method on it. I would ensure that you are properly setting the ref. – Ben Bud Apr 12 '18 at 15:17
  • 2
    @BenBud this breaks with nested Portals —  Example — https://codesandbox.io/s/yvlm8lor7v Explanation (twitter thread) — https://twitter.com/gdad_s_river/status/987725719215144960 I could use some suggestions to improve this component to work well with portals. – Arihant Apr 23 '18 at 09:27
  • 30
    I get the following error: "this.wrapperRef.contains is not a function" – Bogdan May 15 '18 at 12:01
  • 11
    @Bogdan , I was getting the same error when using a styled-component. Consider using a
    on the top level
    – user3040068 May 26 '18 at 23:29
  • 2
    Anyone with @Bogdan's problem, the solution suggested is correct: just add a container div and place the ref attribute there instead of your actual component. That works. – James Stone Jul 02 '18 at 15:12
  • @gdad-s-river, for nested portals I recommend setting the close handler for each portal on a full width/height div rendered underneath each nested portal. This way you can track which one needs to close. Don't think there is a better workaround for this ATM. – Ben Bud Jul 13 '18 at 18:30
  • Don't forget to set useCapture = false. Otherwise click from other elements with click listeners will not work as expected – pravin Nov 11 '18 at 14:37
  • It's now better to use componentDidMount() instead of UNSAFE_componentWillMount(). – williamli Dec 04 '18 at 04:44
  • @pravin false is the default value of useCapture. https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener – munkychop Dec 12 '18 at 07:07
  • What about `touchstart` event? Does this work in mobile. Sr im typing via smartphone and not be in front of my pc – Sang Dec 17 '18 at 01:37
  • [1k github stars repo's code](https://github.com/Pomax/react-onclickoutside/blob/master/src/index.js). Instead, this repo uses `touchstart` and `touchmove` events. Hope that there will be someones discuss with me about this – Sang Dec 17 '18 at 01:39
  • 1
    It uses both touch events and mouse events @transang. They are not mutually exclusive. In addition, looks like this library is still using `findDOMNode` which is discouraged. In other news, I'll be updating this shortly to match the latest ref pattern. Just to keep it up to date :-) – Ben Bud Dec 18 '18 at 17:47
  • I have found it too. I decided to impl my self the component (support ignoring elm scrollbar when clicked). This [airbnb code](https://github.com/airbnb/react-outside-click-handler/blob/master/src/OutsideClickHandler.jsx) is also a good source though – Sang Dec 18 '18 at 23:53
  • how to exclude the click occurring on the scrollbar? – Webwoman May 06 '19 at 21:31
  • 1
    Maybe this will help someone else, make sure to set the ref on a DOM element, not a react component. – Davey Sep 12 '19 at 09:27
  • Is it a good practice to touch DOM in react? Here you have used `document` – ashwintastic Sep 25 '19 at 09:08
  • 1
    prefer the click event than the mousedown event: https://github.com/mui-org/material-ui/issues/15724 – Olivier Tassinari Nov 27 '19 at 16:23
  • What do I pass into the ref of OutsideAlerter? – Theorder Dec 22 '19 at 00:00
  • 2
    For the hook implementation, why are you readding and removing the listener on every render, why not just when it's first mounted by passing `[]` as the second argument to useEffect? – Jordan Feb 17 '20 at 19:55
  • 1
    Good catch @Jordan. Updated dep to be the ref. – Ben Bud Mar 23 '20 at 15:48
  • Alternatively you could also use the e.target.closest(query) method. – Jan-Philipp Marks Jul 02 '20 at 17:47
  • 9
    Thanks for this. It worked for some of my cases, but not for a popup that was `display: absolute` - it would always fire `handleClickOutside` when you click anywhere, including inside. Ended up switching to react-outside-click-handler, which seems to cover this case somehow – xaphod Jul 24 '20 at 15:22
  • `mousedown` not work on mobile mode, that : https://stackoverflow.com/a/63095104/9536897 – Or Choban Jul 26 '20 at 01:03
  • 3
    This answer misses the `setWrapperRef` method. Thankfully it can be found at https://codesandbox.io/s/30q3mzjv91?module=%2Fsrc%2FOutsideAlerter.js. – Arvind K. Aug 07 '20 at 13:52
  • 1
    You may need an additional check of `typeof this.wrapperRef.current !== 'undefined'` inside `handleClickOutside` method to avoid `TypeError: Cannot read property 'contains' of null`. Also you may need to remove `current` property while looking for `contain`. i ended up with this, `handleClickOutside(event) { if (this.wrapperRef && typeof this.wrapperRef.current === 'undefined' && !this.wrapperRef.contains(event.target)) { alert('You clicked outside of me!'); } }` – Arvind K. Aug 07 '20 at 15:29
  • 1
    I tried to use a class implementation but this solution doesn't work for me. I've got outside event click in any case, when I clicked outside the component and even I clicked on the component. – Troyan Victor Aug 11 '20 at 19:02
  • There is a custom hook available from https://www.npmjs.com/package/@rooks/use-outside-click. Just need to pass in ref of element to whose outside click needs to be listened and a function to call when event is captured – Bhupendra Sep 04 '20 at 10:20
  • import PropTypes from 'prop-types'; – Brr Switch Sep 11 '20 at 18:55
  • When using this, on a not even that big react site, it kills performance. It takes 1s for a context menu to display when right clicking – Barry Chapman Oct 22 '20 at 00:51
  • I had the error `React Hook “useEffect” is called in function (...)` come up when trying this code. Turned out that I replaced the `useOutsideAlerter` function name for something that didn't start with `use`, and that made it crash, in case that happens to anyone else. – Diego M.F. Nov 17 '20 at 19:45
  • In case anyone gets stuck on this... this ref property has to be on an HTML element like a div. If you put the ref attribute on a React component, this example doesn't work. – idungotnosn Jan 12 '21 at 17:36
  • 1
    The TypeScript Hooks Implementation: `function useOutsideAlerter(ref: React.RefObject) { useEffect(() => { function handleClickOutside(event: MouseEvent) { if (ref.current && !ref.current.contains(event.target as Element)) { alert("You clicked outside of me!"); } } ..... ` – prograils Feb 18 '21 at 16:13
  • Very nice answer! – Armandas Barkauskas Mar 06 '21 at 23:28
  • 1
    @ShreyaB, this won't work in react-native, you'd have to change the hook to not rely on `document` as the dom is only available in react for the web. – Ben Bud Apr 30 '21 at 15:28
  • When clicking anywhere outside of that component, if the part where you clicked has a click event attached to it, that also would be triggered. How do you prevent that? Even using e.stopPropagation() did not work @BenBud – PCK May 06 '21 at 04:15
  • Quick note that you might want to add a timeout/delay before attaching the outer click handler, to avoid potentially dismissing the modal via the same click event that opened it. (See here for a code example: https://github.com/facebook/react/blob/0eea5772486318c5b2922c8b36680cf4744615d6/packages/react-devtools-shared/src/devtools/views/hooks.js#L200-L257) – bvaughn Jun 07 '21 at 13:04
  • how to detect the click inside an iframe? document listener doesn't detect iframe mousedown events – Kelei Ren Oct 31 '21 at 10:24
  • @Bogdan, and others who are seeing issues with the class implementation, you are most likely seeing this issue because you are on a react version before 16.3. Please reference the updated answer that includes Pre and Post 16.3 implementations. – Ben Bud Mar 10 '22 at 17:07
  • 1
    I made the event listener like `"ontouchstart" in window ? "touchstart" : "mousedown";` and it worked fine on mobile devices. – karutt Mar 14 '22 at 22:33
  • following the same logic https://usehooks.com/useOnClickOutside/ – abernier Mar 30 '22 at 16:33
  • This will not work if element inside modal gets destroyed on click event, in such case it will lose its parent node, but document event will continue execution which will close the modal – Nick May 18 '22 at 09:55
  • why is ref used as a dependency of `useEffect()`? I don't think that this should be done. – Minh Nghĩa Jun 03 '22 at 02:51
  • for better support across desktop and mobile listen to both the `mousedown` and `touchstart` events; with only mousedown the handler doesn't fire for me on iOS Safari when certain"outside" elements are clicked – Grant Humphries Nov 02 '22 at 22:47
  • Great!, but I would rather use `mouseup` event. Because if You have multiple same components you can potentially have issues with event capturing and bubbling (as I have had). – Ignatella Dec 17 '22 at 22:24
  • I was getting an error that `this.wrapper.current was null` so I changed the `handleClickOutside` method to the following `handleClickOutside(event) { if(this.wrapperRef.current === null) return if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) { this.setState({"update": false}) } }` – Ando Apr 26 '23 at 09:40
281

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.

Paul Fitzgerald
  • 11,770
  • 4
  • 42
  • 54
  • Great solution but technically it's a duplicate of https://stackoverflow.com/a/42234988/4286919 – Lee Han Kyeol Sep 18 '17 at 06:39
  • 1
    @LeeHanKyeol Not entirely - this answer invokes the event handlers during the capture phase of event handling, whereas the answer linked to invokes the event handlers during the bubble phase. – stevejay Dec 03 '17 at 10:33
  • 12
    This should be the accepted answer. Worked perfectly for dropdown menus with absolute positioning. – dishwasherWithProgrammingSkill Dec 13 '17 at 17:53
  • 4
    `ReactDOM.findDOMNode` and is deprecated, should use `ref` callbacks: https://github.com/yannickcr/eslint-plugin-react/issues/678#issue-165177220 – dain Mar 01 '18 at 13:09
  • Updating visibility to `false` in the first example fires immediately if the component is originally hidden using the `hidden` HTML attribute – 7ahid Apr 22 '19 at 17:36
  • 1
    This worked for me although I had to hack your component to reset visible back to 'true' after 200ms (otherwise my menu never shows again). Thank you! – Lee Maan Apr 01 '20 at 17:52
  • Very nice solution, but I'd recommend to use this line inside handleClickOutside instead: if (ref.current && !ref.current.contains(event.target) && isComponentVisible) Otherwise, the setIsComponentVisible(false) call will run every time something is clicked even though the component is not visible anyway. – Marnix.hoh Apr 07 '20 at 14:11
  • @Marnix.hoh if state doesn't change the setState will not be fired – Alfred Feb 12 '21 at 16:16
  • 1
    Really like this. Note in the hook approach, I had to add `else { setIsComponentVisible(true); }` in `handleClickOutside()`, otherwise the component would hide but never show. – majorobot Apr 28 '21 at 19:54
  • 1
    @Paul this is a nice solution. Worked for me and didn't have to install any extra library. +1 for this. – Souvik Ray May 25 '21 at 09:19
  • I love this hooks solutions, very elegant. @majorobot - with regards to your issue, I would probably just show and hide it normally using `setIsComponentVisible` in your main component. I had to make sure to apply the `ref` to a slightly wider component otherwise it would cancel out the hiding behaviour of my main button toggle – Matt Fletcher Jul 01 '21 at 19:48
  • by the way I use Typescript, and on `event` what type should I pass? I don't want to pass `any` – callmenikk Nov 03 '21 at 13:31
  • Does this useEffect really need to run on every render? Cant it just contain an empty array [] and run on mount/unmount? – DannyMoshe Jan 06 '22 at 13:55
  • 2
    this is the typing setup I've got with TS which seems to work: `const ref = useRef(null); const handleClickOutside = (event: MouseEvent) => { const target = event.target as HTMLElement; if (ref.current && !ref.current.contains(target)) { setIsComponentVisible(false); }};` – fredrivett Jul 30 '22 at 11:25
  • 1
    How to use this hook solution in a loop? For example I have a table and each row has a dropdown, then `ref` will always point to the last row. – Alex Pavlov Apr 26 '23 at 11:32
250

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>
        );
    }
});
Pablo Barría Urenda
  • 5,703
  • 5
  • 20
  • 32
  • 18
    Wouldn't this create problems with accessibility? Since you make an extra element focusable just to detect when it's going in/out focus, but the interaction should be on the dropdown values no? – Thijs Koerselman Aug 05 '16 at 08:06
  • Accessibility wasn't a concern of that particular widget. I'm not an expert on the subject, but I believe that any accessibility concerns could be addressed with a more sophisticated version of this approach. Thanks for pointing it out. – Pablo Barría Urenda Aug 06 '16 at 10:21
  • 2
    This is a very interesting approach. Worked perfectly for my situation, but I'd be interested to learn how this might affect accessibility. – klinore Sep 01 '16 at 14:25
  • 28
    I have this "working" to an extent, its a cool little hack. My issue is that my drop down content is `display:absolute` so that the drop down won't affect the parent div's size. This means when I click an item in the dropdown, the onblur fires. – Dave Oct 07 '16 at 17:37
  • 1
    I agree with Dave, this is a cool and easy way to do it buy when it comes to element that has an absolute position this won't do much help. – mateeyow Dec 09 '16 at 10:25
  • 5
    I had issues with this approach, any element in the dropdown content doesn't fire events such as onClick. – AnimaSola Jan 03 '17 at 05:56
  • 1
    I'm having the same issue as @Dave and @mateeyow have mentioned above: when dropdown is `position: absolute` clicking a link in the dropdown causes the onBlur to fire before the link to the dropdown is followed. – Beau Smith Jan 28 '17 at 00:09
  • For absolute positioned element, see my answer: http://stackoverflow.com/a/42234674/5059407 – Anthony Garant Feb 14 '17 at 19:34
  • 11
    You might face some other issues if the dropdown content contains some focusable elements like form inputs for example. They'll steal your focus, `onBlur ` will be triggered on your dropdown container and `expanded ` will be set to false. – Kamagatos Jul 04 '17 at 04:47
  • @Kamagatos, you are right for this case. Do you have a suggestion how this situation can be handled? – elenaHristova Jan 15 '18 at 11:17
  • Worked like a charm... THX! – sneridagh Mar 03 '18 at 21:43
  • This is great, except on iOS devices onBlur never fires. When you click/tap away, the element will lose its hover state, but remains focused. You'll have to listen for touch events as well if you want this to work on iOS. – Leeland Miller May 17 '18 at 20:57
  • 4
    For clicking on elements inside your target, see this answer: https://stackoverflow.com/a/44378829/642287 – Onosa Nov 08 '18 at 15:56
  • This solution fit for me enough. I used tabIndex attr and onBlur event and everything works perfectly. Thank you for this simple solution! – Troyan Victor Aug 11 '20 at 19:04
  • this is 3411444k of iq – Jesús Apr 10 '21 at 04:13
  • That's an awesome little trick. I used tabIndex attr and onBlur event and everything works perfectly. Thank you for this simple but great time-saving solution! – Sandip Dhang Jul 01 '21 at 08:32
  • This implementation works best for my use case cause I need onBlur to fire if you click outside of the current browser window – Ice_mank Nov 11 '21 at 15:47
116

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.

Beau Smith
  • 33,433
  • 13
  • 94
  • 101
  • 2
    Are there any concrete advantages to this over the [`tabIndex`/`onBlur` approach proposed by Pablo](http://stackoverflow.com/a/37491578/1709587)? How does implementation work, and how does its behaviour differ from the `onBlur` approach? – Mark Amery Jan 17 '17 at 15:20
  • 6
    Its better to user a dedicated component then using a tabIndex hack. Upvoting it! – Atul Yadav Jan 23 '17 at 18:55
  • 10
    @MarkAmery - I put a comment on the `tabIndex/onBlur` approach. It doesn't work when the dropdown is `position:absolute`, such as a menu hovering over other content. – Beau Smith Jan 28 '17 at 00:11
  • 5
    Another advantage of using this over tabindex is that the tabindex solution also fire a blur event if you focus on a child element – Jemar Jones Apr 19 '17 at 19:24
  • Additionally, onBlur doesn't fire on iOS when clicking/tapping away from the focused element. Tapping away only removes hover state, while remaining focused. – Leeland Miller May 17 '18 at 20:59
  • Fixed my issue (with pop-ups that wouldn't un-pop-up) in seconds, thanks! – Ade Mar 10 '20 at 10:22
110

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 lean Client API
  • stable click listener for lifetime of containing component ([] deps)
  • Client can set callback without needing to memoize it by useCallback
  • 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

ford04
  • 66,267
  • 20
  • 199
  • 171
  • No errors but doesnt work on chrome, iphone 7+. It doesnt detect the taps outside. In chrome dev tools on mobile it works but in a real ios device its not working. – Omar Jun 21 '19 at 22:13
  • @Omar seems to be a very specific IOS oddity. Look here: https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html ,https://stackoverflow.com/questions/18524177/strange-click-event-bubble-on-iphone-safari-that-it-stop-before-bubbling-to-docu , https://gravitydept.com/blog/js-click-event-bubbling-on-ios. The simplest workaround I can imagine is this: In the codesandbox sample, set an empty click handler for the root div like this: `
    {}}> ...
    `, so that IOS registers all clicks from the outside. It works for me on Iphone 6+. Does that solve your problem?
    – ford04 Jun 22 '19 at 11:44
  • i will need to test. but thanks for the solution. Doesnt it seem clunky though? – Omar Jun 23 '19 at 03:35
  • No Worries. Yeah, its seems to be really general behavior of IOS concerning click listeners (and I don't agree with that). IOS treats only specific elements like form fields as clickable. And to check for outer clicks, you will need to have some base element to listen for all clicks, so there is no choice. I agree with you that its clunky. More elegant solution to above is to encapsulte the logic (registering the empty click listener for some root element) in the hook itself and to make sure that its only active for the IOS build. I'll update that, if your test works! – ford04 Jun 23 '19 at 08:56
  • 2
    @ford04 thank your for digging that out, i stumbled upon another [thread](https://stackoverflow.com/questions/10574228/javascript-jquery-tap-outside-an-element-on-an-iphone) that was suggesting the following trick. `@media (hover: none) and (pointer: coarse) { body { cursor:pointer } }` Adding this in my global styles, seems it fixed the problem. – Kostas Sarantopoulos Jul 02 '19 at 15:30
  • Interesting @KostasSarantopoulos ! I am not an expert on media queries - I guess it ensures to only be active for mobile devices? So your example looks even more minimal invasive while still permitting document click listeners - cool thing :) – ford04 Jul 02 '19 at 20:36
  • 1
    @ford04 yeah, btw according to [this thread](https://stackoverflow.com/questions/30102792/css-media-query-target-only-ios-devices) there is an even more specific media query `@supports (-webkit-overflow-scrolling: touch)` that targets only IOS even though i don't know more how much! I just tested it and it works. – Kostas Sarantopoulos Jul 03 '19 at 15:24
  • why we're using a ref to store the callback ? // set current callback in ref, before second useEffect uses it useEffect(() => { // useEffect wrapper to be safe for concurrent mode callbackRef.current = callback; }); – Viraj Singh Sep 11 '21 at 11:45
  • @VirajSingh otherwise, `callback` always would be the stale value from first invocation, as we create a stable click listener via `[]` deps. Take a look at the provided links for a bit more info on that topic. – ford04 Sep 15 '21 at 19:51
63

[Update] Solution with React ^16.8 using Hooks

CodeSandbox

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:

CodeSandbox

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;
onoya
  • 2,268
  • 2
  • 17
  • 17
  • 5
    This is the go-to solution now. If you're getting the error `.contains is not a function`, it may be because you're passing the ref prop to a custom component rather than a real DOM element like a `
    `
    – willlma Nov 14 '18 at 00:24
  • For those trying to pass `ref` prop to a custom component, you may want to have a look at `React.forwardRef` – onoya Jul 18 '19 at 05:44
  • when you click on or hold scrollbar, it affects too. How to make it detect click outside but not scrollbar? – Kien Pham Jan 02 '22 at 02:32
  • The `.contains` error (in this case `'contains' does not exist on type 'never'`) can also appear if you don't type useRef(), e.g.: `const ref = useRef(null)`. –  HigoChumbo Jun 08 '22 at 01:29
61

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.

Niyaz
  • 53,943
  • 55
  • 151
  • 182
  • Very good! Thanks. But in my case it was a problem to catch "current place" with position absolute for the div, so I use "OnMouseLeave" for div with input and drop down calendar to just disable all div when mouse leave the div. – Alex Oct 12 '17 at 08:22
  • 3
    Thanks! Help me a lot. – Angelo Lucas Aug 29 '18 at 17:31
  • 3
    I like this better than those methods attaching to `document`. Thanks. – kyw Mar 21 '19 at 06:02
  • 5
    For this to work you need to make sure that the clicked element (relatedTarget) is focusable. See https://stackoverflow.com/questions/42764494/blur-event-relatedtarget-returns-null – xabitrigo May 13 '20 at 08:32
  • 3
    Should honestly be the accepted answer, cleanest solution ITT – whirish Jan 10 '21 at 02:41
44

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>
    )
  }
}
Bhargav Ponnapalli
  • 9,224
  • 7
  • 36
  • 45
Thijs Koerselman
  • 21,680
  • 22
  • 74
  • 108
  • 8
    This does not work for me any more with React 0.14.7 - maybe React changed something, or maybe I made an error when adapting the code to all the changes to React. I'm instead using https://github.com/Pomax/react-onclickoutside which works like a charm. – Nicole Apr 05 '16 at 11:40
  • 1
    Hmm. I don't see any reason why this should work. Why should a handler on a the app's root DOM node be guaranteed to fire before a rerender triggered by another handler if one on the `document` isn't? – Mark Amery Apr 02 '17 at 08:38
  • 2
    A pure UX point: `mousedown` would probably be a better handler than `click` here. In most applications, the close-menu-by-clicking-outside behaviour happens the moment that you mouse down, not when you release. Try it, for instance, with Stack Overflow's *flag* or *share* dialogues or with one of the dropdowns from your browser's top menu bar. – Mark Amery Apr 02 '17 at 08:42
  • `ReactDOM.findDOMNode` and string `ref` are deprecated, should use `ref` callbacks: https://github.com/yannickcr/eslint-plugin-react/issues/678#issue-165177220 – dain Mar 01 '18 at 13:09
  • this is the most simple solution and works perfectly for me even when attaching to the `document` – Flion Mar 07 '18 at 23:47
37

The Ez way... (UPDATED 2023)

  • Create a hook: useOutsideClick.ts
export function useOutsideClick(ref: any, onClickOut: () => void, deps = []){
    useEffect(() => {
        const onClick = ({target}: any) => !ref?.contains(target) && onClickOut?.()
        document.addEventListener("click", onClick);
        return () => document.removeEventListener("click", onClick);
    }, deps);
}
  • Add componentRef to your component and call useOutsideClick
export function Example(){

  const ref: any = useRef();

  useOutsideClick(ref.current, () => {
    // do something here
  });

  return ( 
    <div ref={ref}> My Component </div>
  )
}
Kuza Grave
  • 1,256
  • 14
  • 15
34

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.

Olivier Tassinari
  • 8,238
  • 4
  • 23
  • 23
19

Alternatively:

const onClickOutsideListener = () => {
    alert("click outside")
    document.removeEventListener("click", onClickOutsideListener)
  }

...

return (
  <div
    onMouseLeave={() => {
          document.addEventListener("click", onClickOutsideListener)
        }}
  >
   ...
  </div>
Gregoire Cattan
  • 555
  • 7
  • 19
13

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>
  ) 
 }
Erhan Namal
  • 159
  • 1
  • 4
9
import { useClickAway } from "react-use";

useClickAway(ref, () => console.log('OUTSIDE CLICKED'));
borchvm
  • 3,533
  • 16
  • 44
  • 45
Melounek
  • 764
  • 4
  • 20
8

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();
                                        }}/>

`

Anthony Garant
  • 614
  • 7
  • 13
  • The issue with this approach is that you can't have any clicks on the Content - the div will receive the click instead. – rbonick Jun 01 '17 at 22:12
  • Since the content is in the div if you do not stop the event propagation both will receive the click. This is often a desired behavior but if you do not want the click to be propagated to the div, simply stop the eventPropagation on your content onClick handler. I have update my answer to show how. – Anthony Garant Jun 02 '17 at 23:45
  • This will not allow you to interact with other elements on your page though, since they will be covered by the wrapper div – whirish Jan 10 '21 at 02:33
  • 2
    This solution should be more upvoted. One of its main advantage, beyond being easy to implement, is that it disable interaction with other element. This is the default behavior of native alert & dropdown and thus should be used for all custom implementation of modal & dropdown. – Raphaël Feb 02 '21 at 10:19
  • To be able to interact with any of desired components while wrapper is active just have higher z-index for those components that you want to interact with. – Lygis Dec 27 '21 at 15:35
7

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>
    );
  }
}
Adam Pietrasiak
  • 12,773
  • 9
  • 78
  • 91
  • Great solution - for my use, I changed to listen to document instead of body in case of pages with short content, and changed ref from string to dom reference: https://jsfiddle.net/agymay93/9/ – Billy Moon May 09 '17 at 11:58
  • and used span instead of div as it's less likely to change layout, and added demo of handling clicks outside of body: https://jsfiddle.net/agymay93/10/ – Billy Moon May 09 '17 at 12:06
  • String `ref` is deprecated, should use `ref` callbacks: https://reactjs.org/docs/refs-and-the-dom.html – dain Mar 01 '18 at 13:10
7

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/

Mohamad
  • 79
  • 1
  • 2
6

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>
    )
  }
}
Yogi
  • 1,527
  • 15
  • 21
Ragav Y
  • 1,662
  • 1
  • 18
  • 32
6

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;
Richard Herries
  • 280
  • 3
  • 4
6

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)
}
Sebastian Nielsen
  • 3,835
  • 5
  • 27
  • 43
  • 1
    This is my favorite approach. I like that it reduces duplicated code and it also enforces putting all the logic on the trigger – Berci Aug 26 '22 at 08:38
5

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>
  );
}

Brendt
  • 51
  • 1
  • 1
4

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 }}
        />
    )
}
4

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>
    )
}

Zak
  • 860
  • 16
  • 39
4

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>
  );
}
Abdelatif D.
  • 61
  • 1
  • 4
4

If you need typescript version:

import React, { useRef, useEffect } from "react";

interface Props {
  ref: React.MutableRefObject<any>;

}

export const useOutsideAlerter = ({ ref }: Props) => {
  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      if (ref.current && !ref.current.contains(event.target as Node)) {
       //do what ever you want
      }
    };
    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref]);
};
export default useOutsideAlerter;

If you want to extend this to close a modal or hide something you can also do:

import React, { useRef, useEffect } from "react";

interface Props {
  ref: React.MutableRefObject<any>;
  setter: React.Dispatch<React.SetStateAction<boolean>>;
}

export const useOutsideAlerter = ({ ref, setter }: Props) => {
  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      if (ref.current && !ref.current.contains(event.target as Node)) {
        setter(false);
      }
    };
    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref, setter]);
};
export default useOutsideAlerter;

Sundar Gautam
  • 445
  • 1
  • 6
  • 10
2

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;
    }
}
metroninja
  • 29
  • 2
2
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

lmiguelvargasf
  • 63,191
  • 45
  • 217
  • 228
Ivan Sanchez
  • 546
  • 5
  • 17
  • 3
    Make sure you remove the event listener when the component unmounts to prevent memory management issues, etc. – agm1984 Jan 05 '18 at 23:07
2

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.

ErichBSchulz
  • 15,047
  • 5
  • 57
  • 61
  • This works fantastically! Far simpler than many other answers here and unlike at least 3 other solutions here where for all of them I recieved `"Support for the experimental syntax 'classProperties' isn't currently enabled"` this just worked straight out the box. For anyone trying this solution, remember to erase any lingering code you may have copied over when trying the other solutions. e.g. adding event listeners does seem to conflict with this. – Frikster Oct 18 '18 at 06:54
  • There is an updated version of this [here](https://github.com/Pomax/react-onclickoutside) – Ice_mank Nov 11 '21 at 15:46
2

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>
  );

};

Link to demo

Partially inspired by @pau1fitzgerald answer.

Ben Carp
  • 24,214
  • 9
  • 60
  • 72
2

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>
  );
}
Ilyas Assainov
  • 1,901
  • 13
  • 15
  • Very useful answer. The moment I separated both refs, the onClickOutside function is not triggered when I click any of the two components – Azeezat Raheem Jun 23 '21 at 21:15
2

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>
  );
}
1

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

kidroca
  • 3,480
  • 2
  • 27
  • 44
1

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>
 )
}
1

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!

Jan-Philipp Marks
  • 1,419
  • 8
  • 13
1

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])
}

ruucm
  • 493
  • 7
  • 15
1

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>

Chetan Jain
  • 236
  • 6
  • 16
1

Since for me the !ref.current.contains(e.target) wasn't working because the DOM elements contained inside the ref were changing, I came up with a slightly different solution:

function useClickOutside<T extends HTMLElement>(
  element: T | null,
  onClickOutside: () => void,
) {
  useEffect(() => {
    function handleClickOutside(event: MouseEvent) {
      const xCoord = event.clientX;
      const yCoord = event.clientY;

      if (element) {
        const { right, x, bottom, y } = element.getBoundingClientRect();
        if (xCoord < right && xCoord > x && yCoord < bottom && yCoord > y) {
          return;
        }

        onClickOutside();
      }
    }

    document.addEventListener('click', handleClickOutside);
    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, [element, onClickOutside]);
  • I used the Hook solution, but also thanks to your solution, I used yCoord to check it was below an un-managed Header. Cheers. – thomasL Sep 27 '22 at 08:31
1

Try this it works perfectly

import React, { useState, useRef, useEffect } from "react";

const Dropdown = () => {

  const [state, setState] = useState(true);
  const dropdownRef = useRef(null);

  const handleToggleDropdown = () => {
    if (state) setState(false);
    else setState(true);
  }

  const handleClickOutside = (event) => {
    if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
      setState(true);
    }
  }

  useEffect(() => {
    document.addEventListener('mousedown', handleClickOutside);
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    }
  }, []);

  return (
    <React.Fragment>
      <div className="dropdown" ref={dropdownRef}>
        <button onClick={() => handleToggleDropdown()}>
          Options
        </button>
        <div className="dropdown-content" hidden={state}>


        </div>
      </div>
    </React.Fragment>
  );
}


export default Dropdown;
1

The most simple solution I have found is to use:

onBlur={(e) => {
  const parent = e.currentTarget.parentNode;
  const isDescendant = parent
    ? parent.contains(e.relatedTarget)
    : false;
  if (!isDescendant) {
    setIsOpen(false);
  }
}}

You might have to use multiple .parentNode until you find the dropdown root element (parent). If e.relatedTarget is not a descendant of parent then you can close the click was outside.

Remo H. Jansen
  • 23,172
  • 11
  • 70
  • 93
1

In Typescript Case

  1. Create Hook, we assume useOutsideAlerter

    const useOutsideAlerter = (ref: RefObject<HTMLElement>, callback: () => void) => {
        useEffect(() => {
            const handleClickOutside = (event: MouseEvent) => {
                if (ref.current && !ref.current.contains(event.target as Node)) {
                    callback()
                }
            }
    
            document.addEventListener('mousedown', handleClickOutside)
            return () => {
                // Unbind the event listener on clean up
                document.removeEventListener('mousedown', handleClickOutside)
            }
        }, [ref])
    }
    
  2. Call Hook In Your Component

    const MyComponent: FC<{}> = (props) => {
        const wrapperRef = useRef(null)
    
        const handleOutSideClick = () => {
            alert('click outside component')
        }
    
        const handleInSideClick = () => {
            alert('click inside component')
        }
    
        useOutsideAlerter(wrapperRef, handleOutSideClick)
    
        return (
            <div ref={wrapperRef}><button onClick={handleInSideClick}>KLIK ME</button></div>
        )
    }
    
1

There is an npm module which will make your life easier to handle the clicks outside a specific component. For Example: You make states as true and false. On dropdown a menu list your state is true and on clicking on close button your state converts to false and dropdown menu component gets disappear. But you want to close this drop down menu also clicking on outside of the drop down menu on the window. To deal with such scenario follow the below steps:

 npm i react-outside-click-handler

Now Import this module in your React File:

import OutsideClickHandler from 'react-outside-click-handler';

Now You have imported a component from this module. This component takes a component outside of which you want to detect a click event

function MyComponent() {
  return (
    <OutsideClickHandler
      onOutsideClick={() => {
        alert("You clicked outside of this component!!!");
        //Or any logic you want
      }} >
      <yourComponent />
    </OutsideClickHandler>
  );
}

Now Simply replace you Own component with . I hope this find you helpful :)

0

I found this from the article below:

render() { return ( { this.node = node; }} > Toggle Popover {this.state.popupVisible && ( I'm a popover! )} ); } }

Here is a great article about this issue: "Handle clicks outside of React components" https://larsgraubner.com/handle-outside-clicks-react/

0

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>
        );
    }
}
wdm
  • 7,121
  • 1
  • 27
  • 29
  • Not sure who's downvoting but this solution is pretty clean compared to binding/unbinding event listeners. I've had zero issues with this implementation. – wdm Jan 27 '20 at 22:12
  • also thought about this approach. I'm concern only by splitting the logic between the child and parent. besides, apps have many levels so does this mean we need to handle all the clicks in all those levels outside our component? – Pam Stums Jul 21 '22 at 16:37
  • This works great if you have only 2 components. Where you only care about click on the parent that affects the child. So clicking on other components should also increase the "click-count" – Pam Stums Jul 21 '22 at 16:39
0

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 ;)

Mojtaba Darzi
  • 328
  • 5
  • 10
0

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.
Gorjan Mishevski
  • 182
  • 3
  • 10
Or Choban
  • 1,661
  • 1
  • 8
  • 9
0

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.

ggogobera
  • 1
  • 2
0
import React, { useState, useEffect, useRef } from "react";

const YourComponent: React.FC<ComponentProps> = (props) => {
  const ref = useRef<HTMLDivElement | null>(null);
  const [myState, setMyState] = useState(false);
  useEffect(() => {
    const listener = (event: MouseEvent) => {
      // we have to add some logic to decide whether or not a click event is inside of this editor
      // if user clicks on inside the div we dont want to setState
      // we add ref to div to figure out whether or not a user is clicking inside this div to determine whether or not event.target is inside the div
      if (
        ref.current &&
        event.target &&
        // contains is expect other: Node | null
        ref.current.contains(event.target as Node)
      ) {
        return;
      }
      // if we are outside
      setMyState(false);
    };
    // anytime user clics anywhere on the dom, that click event will bubble up into our body element
    // without { capture: true } it might not work
    document.addEventListener("click", listener, { capture: true });
    return () => {
      document.removeEventListener("click", listener, { capture: true });
    };
  }, []);

  return (
    <div  ref={ref}>
      ....
    </div>
  );
};
Yilmaz
  • 35,338
  • 10
  • 157
  • 202
0
import { RefObject, useEffect } from 'react';

const useClickOutside = <T extends HTMLElement>(ref: RefObject<T>, fn: () => void) => {
    useEffect(() => {
        const element = ref?.current;
        function handleClickOutside(event: Event) {
            if (element && !element.contains(event.target as Node | null)) {
                fn();
            }
        }
        document.addEventListener('mousedown', handleClickOutside);
        return () => {
            document.removeEventListener('mousedown', handleClickOutside);
        };
    }, [ref]);
};

export default useClickOutside;
  • Your `useEffect` is missing `fn` as a dependency, thus if it changes for some reason, the `useEffect` will not update and you will be referencing a stale version of it. – reubennn Mar 26 '23 at 20:50
0

All the solution proposed assume that an event can be added to the document and rely on the native method .contains() to distinguish if the event is triggered inside or outside of the current component

ref.current.contains(event.target)

but this is not always valid in React. In React, in fact, there is the React.createPortal API that permit to specify from a component a new real parent component in which the JSX is rendered into but, at the same time, the event bubbling is simulated as is the component is rendered in the declared place (i.e. where React.createPortal is invoked).

This is achieved attaching all the event to the app root element and simulating the events in Javascript.

So the solution proposed is broken in that scenario because a click inside a portal element, that for the standard HTML is outside the current element, actually should be handled as it is inside.

So I rewritten a solution proposed in a comment in this question, and refactored it to use the functional component. This works also in case of one or multiple nested portals.

export default function OutsideClickDetector({onOutsideClick, Component ='div', ...props} : OutsideClickDetectorProps) {
    const isClickInside = useRef<boolean>(false);

    const onMouseDown = () => {
        isClickInside.current = true;
    };
    
    const handleBodyClick = useCallback((e) => {
        if(!isClickInside.current) {
            onOutsideClick?.(e);
        }
        isClickInside.current = false;
    }, [isClickInside, onOutsideClick]);

    useEffect(() => {
        document.addEventListener('mousedown', handleBodyClick);
        return () => document.removeEventListener('mousedown', handleBodyClick);
    });

    return <Component onMouseDown={onMouseDown} onMouseUp={() => isClickInside.current = false}{...props} />;
}

alessandro308
  • 1,912
  • 2
  • 15
  • 27
0

In addition to other solution, here is the improved function that can unsubscribe the listener and add an element or group of element of the React component to track the click. This hook can be used to handle scenarios such as closing a dropdown menu when a user clicks outside of it, or hiding a modal when the user clicks outside of it.

function useOutsideClick(
      reference: Array<React.RefObject<any>>,
      onClickOut: () => void,
      shouldListen = true,
    ) {
      useEffect(() => {
        const onClick = (event: Event) => {
          if (
            shouldListen &&
            reference.some(
              (reference_) =>
                reference_.current?.contains(event.target as Node) === false,
            )
          ) {
            return onClickOut();
          }
        };
        if (!shouldListen) {
          return document.removeEventListener('click', onClick);
        }
        document.addEventListener('click', onClick);
        return () => document.removeEventListener('click', onClick);
      }, [reference, shouldListen]);
    }
Allan Tanaka
  • 297
  • 3
  • 11
-1

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>
                }
idtmc
  • 1
  • 1
-1
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");
    }
  }
}
dipole_moment
  • 5,266
  • 4
  • 39
  • 55
-1

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>;
  }
}
-1

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,
};
AnaT
  • 1
-1

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.

Tushar Sharma
  • 192
  • 1
  • 15
-2

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
    }
}
Rogod
  • 188
  • 1
  • 13
-2

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>
    )
}
António Ribeiro
  • 4,129
  • 5
  • 32
  • 49