检测 React 组件外部的点击
Detect click outside React component
我正在寻找一种方法来检测单击事件是否发生在组件之外,如本文所述。 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
[更新] 使用 Hooks 的 React ^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 链接的点击。以下是我如何实现单击外部和关闭的示例。
这使用 ES6 和 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;
我这样做的部分原因是遵循这一点,并遵循 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>
);
}
- 为什么我的外部托管的 React 组件在通过 NPM 加载时会中断
- React.js外部脚本
- 来自外部API的React组件的绑定数据
- 从旧的外部Javascript更改React组件的状态
- 在“运行时”从外部脚本加载React JS组件
- React-只运行文件一旦外部js文件加载(gapi未定义)
- 外部react组件看不到react . component
- 在React中使用外部脚本功能
- 将(外部)组件动态加载到React.js中
- 使用redux单击react- bootstrap中的外部模态
- 我如何添加外部DOM到React组件
- React:如何读取外部onChange事件
- 为什么我需要的外部文件被忽略在React Native
- React:如何加载和渲染外部html文件
- React - Redux -订阅外部事件
- 检测 React 组件外部的点击
- 在React中引用组件外部的变量
- 如何将外部方法传递给React中的无状态功能组件
- 无法从React外部在React组件上设置state
- React用class查找所有组件,从外部更新组件状态或道具.模态叠加