乐闻世界logo
搜索文章和话题

How to detect click outside on react component

7 个月前提问
3 个月前修改
浏览次数27

6个答案

1
2
3
4
5
6

在React中,检测组件外部的点击事件通常可以通过以下几个步骤进行:

  1. 添加全局事件监听器:在组件挂载(componentDidMount 或者 useEffect)后,添加一个点击事件监听器到document上,这样可以监听到所有的点击事件。

  2. 设置引用(Ref):使用useRef创建一个引用,并将其附加到你希望检测外部点击的组件上。这允许我们可以访问真实的DOM节点,以判断点击事件是否发生在其内部。

  3. 检测点击位置:当全局点击事件被触发时,可以使用该事件的target属性,并与我们的组件的DOM节点进行比较,来确定点击是否在组件外部进行。

  4. 清理事件监听器:在组件卸载(componentWillUnmount 或者 useEffect的返回函数)时,要移除事件监听器,避免内存泄漏。

下面是一个使用Hooks实现的例子:

jsx
import React, { useEffect, useRef } from 'react'; function OutsideClickExample() { const wrapperRef = useRef(null); // Step 2 useEffect(() => { // Step 1 function handleClickOutside(event) { if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { // Step 3 console.log('你点击了组件外部'); } } // 在document上添加事件监听器 document.addEventListener('mousedown', handleClickOutside); // Step 4 return () => { // 在组件卸载时移除事件监听器 document.removeEventListener('mousedown', handleClickOutside); }; }, [wrapperRef]); return ( <div ref={wrapperRef}> <p>点击我之外的地方试试看!</p> </div> ); } export default OutsideClickExample;

在这个例子中,useEffect确保了事件监听器仅在组件挂载后添加,并在组件卸载时移除。ref的作用是提供了一种方式来引用实际的DOM元素,从而我们可以判断点击事件是否在这个元素之外发生。注意,这个例子使用了mousedown事件,它会在点击鼠标按钮时立即触发,而不是在释放按钮时(click事件)。根据你的应用场景,你可能需要选择不同的事件类型。

2024年6月29日 12:07 回复

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

要查看它的实际效果:

钩子实现:

shell
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之后

shell
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之前

shell
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>; } }
2024年6月29日 12:07 回复

我被困在同样的问题上。我参加这里的聚会有点晚了,但对我来说,这是一个非常好的解决方案。希望它对其他人有帮助。您需要findDOMNode从以下位置导入react-dom

shell
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 方法 (16.8 +)

您可以创建一个名为 的可重用挂钩useComponentVisible

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

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

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

在这里找到一个codesandbox示例。

2024年6月29日 12:07 回复

2021 年更新:

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

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

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

shell
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> ); } });
2024年6月29日 12:07 回复

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

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

shell
import onClickOutside from 'react-onclickoutside'

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

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

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

shell
export default onClickOutside(NameOfComponent)

就是这样。

2024年6月29日 12:07 回复

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

useOuterClick应用程序编程接口

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

执行

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

这是一个工作示例:

显示代码片段

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

Run code snippetHide results

Expand snippet

关键点

  • useOuterClick利用可变引用来提供精益ClientAPI
  • 在包含组件 ( []deps)的生命周期内_保持稳定的点击侦听器_
  • Client可以设置回调而不需要通过以下方式记住它useCallback
  • 回调主体可以访问最新的 props 和状态 -没有过时的闭包值

(iOS 的旁注)

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

2024年6月29日 12:07 回复

你的答案