检测 React 组件外部的点击

Detect click outside React component

本文关键字:外部 React 组件 检测      更新时间:2023-09-26

我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。 jQuery closest(( 用于查看来自点击事件的目标是否将 dom 元素作为其父元素之一。如果存在匹配项,则 click 事件属于其中一个子项,因此不被视为在组件之外。

所以在我的组件中,我想将一个点击处理程序附加到window。当处理程序触发时,我需要将目标与组件的 dom 子级进行比较。

单击事件包含

诸如"路径"之类的属性,该属性似乎包含事件已行进的 dom 路径。我不确定要比较什么或如何最好地遍历它,我想一定有人已经把它放在一个聪明的实用程序函数中......不?

以下解决方案使用 ES6,并遵循绑定以及通过方法设置 ref 的最佳做法。

要查看它的实际效果,请执行以下操作:

  • 钩子实现
  • React 16.3 之后的类实现
  • React 16.3 之前的类实现

钩子实现:

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

类实现:

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

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

我被困在同一个问题上。我来这里参加派对有点晚了,但对我来说,这是一个很好的解决方案。希望它能对其他人有所帮助。您需要从react-dom导入findDOMNode

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

反应钩子方法 (16.8 +(

您可以创建一个名为 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 };
}

然后在组件中,您希望添加功能以执行以下操作:

const DropDown = () => {
    const { ref, isComponentVisible } = useComponentVisible(true);
    return (
       <div ref={ref}>
          {isComponentVisible && (<p>Dropdown Component</p>)}
       </div>
    );
 
}

在此处查找代码沙盒示例。

2021 年更新:

自从我添加这个响应以来已经有一段时间了,而且由于它似乎仍然引起了一些兴趣,我想我会将其更新到更新的 React 版本。在 2021 年,这就是我编写此组件的方式:

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

原答案(2016(:

这是最适合我的解决方案,无需将事件附加到容器:

某些HTML元素可以具有所谓的">焦点",例如输入元素。当这些元素失去焦点时,它们也会响应模糊事件。

要使任何元素具有焦点,只需确保其 tabindex 属性设置为 -1 以外的任何值。在常规 HTML 中,这将通过设置 tabindex 属性,但在 React 中您必须使用 tabIndex(请注意大写I(。

您也可以通过JavaScript和element.setAttribute('tabindex',0)

这就是我使用它来制作自定义下拉菜单的目的。

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

在这里尝试了许多方法后,我决定使用 github.com/Pomax/react-onclickoutside,因为它非常完整。

我通过 npm 安装了模块并将其导入到我的组件中:

import onClickOutside from 'react-onclickoutside'

然后,在我的组件类中,我定义了handleClickOutside方法:

handleClickOutside = () => {
  console.log('onClickOutside() method called')
}

导出我的组件时,我将其包装在onClickOutside()中:

export default onClickOutside(NameOfComponent)

就是这样。

基于 Tanner Linsley 在 JSConf Hawaii 2020 上的精彩演讲的 Hook 实现:

useOuterClick接口

const Client = () => {
  const innerRef = useOuterClick(ev => {/*event handler code on outer click*/});
  return <div ref={innerRef}> Inside </div> 
};

实现

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

这是一个工作示例:

/*
  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>

要点

  • useOuterClick利用可变的引用来提供精益Client API
  • 包含组件的生命周期内稳定的点击侦听器([] deps(
  • Client可以设置回调,而无需通过useCallback来记忆它
  • 回调体可以访问最新的 props 和状态 - 没有过时的闭包值

(iOS的旁注(

iOS 通常仅将某些元素视为可点击元素。要使外部点击起作用,请选择与document不同的点击侦听器 - 不包括body。例如,在 React 根div上添加一个侦听器并扩展其高度,如 height: 100vh ,以捕获所有外部点击。来源: quirksmode.org

[更新] 使用 HooksReact ^16.8 解决方案

代码沙盒

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;

使用 React ^16.3 的解决方案:

代码沙盒

import React, { Component } from "react";
class SampleComponent extends Component {
  state = {
    clickedOutside: false
  };
  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }
  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }
  myRef = React.createRef();
  handleClickOutside = e => {
    if (!this.myRef.current.contains(e.target)) {
      this.setState({ clickedOutside: true });
    }
  };
  handleClickInside = () => this.setState({ clickedOutside: false });
  render() {
    return (
      <button ref={this.myRef} onClick={this.handleClickInside}>
        {this.state.clickedOutside ? "Bye!" : "Hello!"}
      </button>
    );
  }
}
export default SampleComponent;

这里的其他答案都不适合我。我试图隐藏模糊的弹出窗口,但由于内容是绝对定位的,即使在单击内部内容时,onBlur 也会触发。

这是一种对我有用的方法:

// 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.
    }
},

希望这有帮助。

多亏了 Ben Alpert discuss.reactjs.org,我找到了解决方案。建议的方法将处理程序附加到文档,但结果证明这是有问题的。单击树中的一个组件会导致重新渲染,从而在更新时删除单击的元素。由于 React 的重新渲染发生在调用文档正文处理程序之前,因此未检测到该元素在树的"内部"。

此问题的解决方案是在应用程序根元素上添加处理程序。

主要:

window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)

元件:

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

Ez 方式...(2023 年更新(

  • 创建钩子: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);
}
  • componentRef添加到组件并调用useOutsideClick
export function Example(){
  const ref: any = useRef();
  useOutsideClick(ref.current, () => {
    // do something here
  });
  return ( 
    <div ref={ref}> My Component </div>
  )
}

MUI 有一个小组件来解决这个问题:https://mui.com/base/react-click-away-listener/你可以挑选它。它的重量低于 1 kB gzipped,它支持移动设备、IE 11 和门户。

或者:

const onClickOutsideListener = () => {
    alert("click outside")
    document.removeEventListener("click", onClickOutsideListener)
  }
...
return (
  <div
    onMouseLeave={() => {
          document.addEventListener("click", onClickOutsideListener)
        }}
  >
   ...
  </div>

带打字稿

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

import { useClickAway } from "react-use";
useClickAway(ref, () => console.log('OUTSIDE CLICKED'));

对于那些需要绝对定位的人来说,我选择的一个简单选项是添加一个包装器组件,该组件的样式设置为用透明背景覆盖整个页面。然后,您可以添加 onClick 此元素以关闭内部组件。

<div style={{
        position: 'fixed',
        top: '0', right: '0', bottom: '0', left: '0',
        zIndex: '1000',
      }} onClick={() => handleOutsideClick()} >
    <Content style={{position: 'absolute'}}/>
</div>

就像现在一样,如果您在内容上添加单击处理程序,该事件也将传播到上层div,从而触发处理程序 OutsideClick。如果这不是您想要的行为,只需停止处理程序上的事件预处理即可。

<Content style={{position: 'absolute'}} onClick={e => {
                                          e.stopPropagation();
                                          desiredFunctionCall();
                                        }}/>

'

这是我

的方法(演示 - https://jsfiddle.net/agymay93/4/(:

我创建了名为 WatchClickOutside 的特殊组件,它可以像(我假设JSX语法(一样使用:

<WatchClickOutside onClickOutside={this.handleClose}>
  <SomeDropdownEtc>
</WatchClickOutside>

这是WatchClickOutside组件的代码:

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

只需使用 mui (material-ui( 的 ClickAwayListener

<ClickAwayListener onClickAway={handleClickAway}>
    {children}
<ClickAwayListener >

有关更多信息,您可以查看:https://mui.com/base/react-click-away-listener/

这已经有很多答案,但它们没有解决e.stopPropagation()问题,也没有阻止单击您希望关闭的元素之外的反应链接。

由于 React 有自己的人工事件处理程序,因此您无法使用文档作为事件侦听器的基础。在此之前,您需要e.stopPropagation(),因为 React 使用文档本身。例如,如果您使用document.querySelector('body')代替。您可以阻止来自 React 链接的点击。以下是我如何实现单击外部和关闭的示例。
这使用 ES6React 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;

我这样做的部分原因是遵循这一点,并遵循 React 官方文档来处理需要 react ^16.3 的 refs。这是在这里尝试其他一些建议后唯一对我有用的东西......

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

打字稿 + @ford04提案的简化版本:

useOuterClick接口

const Client = () => {
  const ref = useOuterClick<HTMLDivElement>(e => { /* Custom-event-handler */ });
  return <div ref={ref}> Inside </div> 
};

实现

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

带钩子的打字稿

注意:我使用的是 React 版本 16.3,带有 React.createRef。 对于其他版本,请使用 ref 回调。

下拉组件:

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

为了扩展Ben Bud的公认答案,如果您使用的是样式组件,则以这种方式传递refs会给您一个错误,例如"this.wrapperRef.contains不是一个函数"。

在注释中建议的修复程序是用div 包装样式化的组件并将 ref 传递给那里,这是有效的。话虽如此,在他们的文档中,他们已经解释了这样做的原因以及在样式组件中正确使用 ref:

将 ref prop 传递给样式化组件将为您提供 StyledComponent 包装器的实例,但不会传递给底层 DOM 节点。这是由于引用的工作方式。不可能直接在我们的包装器上调用 DOM 方法,比如 focus。 要获取对实际包装的 DOM 节点的引用,请将回调传递给 innerRef 道具。

这样:

<StyledDiv innerRef={el => { this.el = el }} />

然后,您可以直接在"handleClickOutside"功能中访问它:

handleClickOutside = e => {
    if (this.el && !this.el.contains(e.target)) {
        console.log('clicked outside')
    }
}

这也适用于"onBlur"方法:

componentDidMount(){
    this.el.focus()
}
blurHandler = () => {
    console.log('clicked outside')
}
render(){
    return(
        <StyledDiv
            onBlur={this.blurHandler}
            tabIndex="0"
            innerRef={el => { this.el = el }}
        />
    )
}

所以我遇到了类似的问题,但就我而言,这里选择的答案不起作用,因为我有一个下拉菜单按钮,嗯,这是文档的一部分。因此,单击该按钮也触发了handleClickOutside功能。为了阻止它触发,我不得不向按钮添加一个新ref,并将此!menuBtnRef.current.contains(e.target)添加到条件。如果有人像我一样面临同样的问题,我会把它留在这里。

下面是组件现在的样子:


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>
    )
}
这是我

解决问题的方法

从我的自定义钩子返回一个布尔值,当这个值发生变化时(如果点击在我作为 arg 传递的 ref 之外,则为 true(,这样我就可以使用 useEffect 钩子捕捉到这个变化,我希望你很清楚。

下面是一个活生生的例子:代码沙盒上的实时示例

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

如果您需要打字稿版本:

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;

如果要扩展它以关闭模式或隐藏某些内容,您还可以执行以下操作:

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;

我对所有其他答案的最大担忧是必须从根/父级过滤点击事件。 我发现最简单的方法是简单地设置一个同级元素,位置:固定,下拉列表后面的 z-index 1,并处理同一组件内固定元素上的单击事件。 将所有内容集中到给定组件。

示例代码

#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;
    }
}
componentWillMount(){
  document.addEventListener('mousedown', this.handleClickOutside)
}
handleClickOutside(event) {
  if(event.path[0].id !== 'your-button'){
     this.setState({showWhatever: false})
  }
}

事件path[0]是最后单击的项目

我使用了这个模块(我与作者没有关联(

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

它做得很好。

UseOnClickOut Hook - React 16.8 +

创建常规用途"外部单击"函数

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, []);

然后在任何功能组件中使用钩子。

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

链接到演示

部分灵感来自@pau1fitzgerald答案。

在我的 DROPDOWN 案例中,Ben Bud 的解决方案运行良好,但我有一个带有 onClick 处理程序的单独切换按钮。因此,外部单击逻辑与按钮单击切换器冲突。以下是我通过传递按钮的 ref 来解决它的方法:

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

我有一个类似的用例,我必须开发一个自定义下拉菜单。 当用户在外部单击时,它应该会自动关闭。 这是最近的 React Hooks 实现——

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