使用hooks在React组件外部检测点击事件
使用hooks在React组件外部检测点击事件
我发现在应用程序中我在多个地方重复使用了一种行为,当用户点击元素外部时,我可以隐藏它。
随着hooks的引入,我是否可以将这个行为放在一个hook中,并在多个组件之间共享,以节省我在每个组件中编写相同逻辑的时间?
我已经在一个组件中实现了这个功能,代码如下。
const Dropdown = () => { const [isDropdownVisible, setIsDropdownVisible] = useState(false); const wrapperRef = useRef(null); const handleHideDropdown = (event: KeyboardEvent) => { if (event.key === 'Escape') { setIsDropdownVisible(false); } }; const handleClickOutside = (event: Event) => { if ( wrapperRef.current && !wrapperRef.current.contains(event.target as Node) ) { setIsDropdownVisible(false); } }; useEffect(() => { document.addEventListener('keydown', handleHideDropdown, true); document.addEventListener('click', handleClickOutside, true); return () => { document.removeEventListener('keydown', handleHideDropdown, true); document.removeEventListener('click', handleClickOutside, true); }; }); return ( Dropdown ); }
在React组件中检测点击外部的问题
问题的原因:
在React组件中,有时候需要检测点击组件外部的事件,比如点击其他地方关闭下拉菜单。通常情况下,我们可以通过给document添加click事件监听来实现。但是在React中,如果直接在组件中添加事件监听,可能会导致事件监听多次创建和销毁的问题,影响性能。
解决方法:
为了解决这个问题,我们可以使用自定义的hooks和一些辅助函数来实现点击外部的检测功能。
首先,我们创建一个useDocumentEvent.js文件,该文件中定义了一个自定义的hooks函数useDocumentEvent,用来添加和移除document的事件监听。这样我们就可以统一管理所有的事件监听,避免重复创建和销毁事件监听。
//useDocumentEvent.js import { useEffect } from 'react' export const useDocumentEvent = (events) => { useEffect( () => { events.forEach((event) => { document.addEventListener(event.type, event.callback) }) return () => events.forEach((event) => { document.removeEventListener(event.type, event.callback) }) }, [events] ) }
然后,我们创建一个useDropdown.js文件,该文件中定义了一个自定义的hooks函数useDropdown,用来实现下拉菜单的功能。这个hooks函数使用了useDocumentEvent函数来添加和移除点击外部的事件监听。
//useDropdown.js import { useCallback, useState, useRef } from 'react' import { useDocumentEvent } from './useDocumentEvent' export const useDropdown = (initialState = false, onAfterClose = null) => { const ref = useRef(null) const [isOpen, setIsOpen] = useState(initialState) const handleClickOutside = useCallback( (event) => { if (ref.current && ref.current.contains(event.target)) { return } setIsOpen(false) onAfterClose && onAfterClose() }, [ref, onAfterClose] ) const handleHideDropdown = useCallback( (event) => { if (event.key === 'Escape') { setIsOpen(false) onAfterClose && onAfterClose() } }, [onAfterClose] ) useDocumentEvent([ { type: 'click', callback: handleClickOutside }, { type: 'keydown', callback: handleHideDropdown }, ]) return [ref, isOpen, setIsOpen] }
最后,我们创建一个Dropdown.js文件,该文件是一个示例组件,使用了上面定义的useDropdown hooks函数来实现下拉菜单的功能。
//Dropdown.js import React, { useState, useEffect } from 'react' import styled from '/styled' import { COLOR } from 'constants/styles' import { useDropdown } from 'hooks/useDropdown' import { Button } from 'components/Button' const Dropdown = ({ children, closeText, openText, ...rest }) => { const [dropdownRef, isOpen, setIsOpen] = useDropdown() const [inner, setInner] = useState(false) const [disabled, setDisabled] = useState(false) const timeout = 150 useEffect(() => { if (isOpen) { setInner(true) } else { setDisabled(true) setTimeout(() => { setDisabled(false) setInner(false) }, timeout + 10) } }, [isOpen]) return () } const DropdownContainer = styled.div( { position: 'absolute', backgroundColor: COLOR.light, color: COLOR.dark, borderRadius: '2px', width: 400, boxShadow: '0px 0px 2px 0px rgba(0,0,0,0.5)', zIndex: 1, overflow: 'hidden', right: 0, }, (props) => ({ transition: props.isVisible ? `all 700ms ease-in-out` : `all ${props.timeout}ms ease-in-out`, maxHeight: props.isVisible ? props.maxHeight || 300 : 0, }) ) export { Dropdown }{inner && children}
最后,我们可以在其他组件中使用Dropdown组件来实现下拉菜单的功能,如下所示:
//.... your code//... more code
以上就是解决React组件中检测点击外部的问题的方法。感谢stackoverflow上的回答和medium上的文章提供的思路和解决方案。
参考链接:
- [stackoverflow上的回答](https://stackoverflow.com/questions/54560790/54570068#54570068)
- [medium上的文章](https://medium.com//little-neat-trick-to-capture-click-outside-with-react-hook-ba77c37c7e82)
- [一篇关于useEffect和useCallback的文章](https://codedaily.io/tutorials/72/Creating-a-Reusable-Window-Event-Listener-with-useEffect-and-useCallback)
在React组件中检测点击外部区域的需求是很常见的,通过使用hooks可以实现这一功能。下面是一个可以实现点击外部区域隐藏组件的可重用hook:
import { useState, useEffect, useRef } from 'react'; export default function useComponentVisible(initialIsVisible) { const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible); const ref = useRef(null); const handleHideDropdown = (event: KeyboardEvent) => { if (event.key === 'Escape') { setIsComponentVisible(false); } }; const handleClickOutside = (event: Event) => { if (ref.current && !ref.current.contains(event.target as Node)) { setIsComponentVisible(false); } }; useEffect(() => { document.addEventListener('keydown', handleHideDropdown, true); document.addEventListener('click', handleClickOutside, true); return () => { document.removeEventListener('keydown', handleHideDropdown, true); document.removeEventListener('click', handleClickOutside, true); }; }); return { ref, isComponentVisible, setIsComponentVisible }; }
上述代码定义了一个名为`useComponentVisible`的hook,它接受一个初始可见性参数`initialIsVisible`。该hook返回一个包含了`ref`、`isComponentVisible`和`setIsComponentVisible`的对象。
在需要添加该功能的组件中,可以按照以下方式使用该hook:
const DropDown = () => { const { ref, isComponentVisible } = useComponentVisible(true); return ({isComponentVisible && (); }Going into Hiding
)}
通过在组件中调用`useComponentVisible`,我们可以获取到一个`ref`,将其绑定到组件中需要点击外部区域隐藏的元素上。通过检测`isComponentVisible`的值,我们可以决定是否显示该元素。
以上代码在codesandbox上提供了一个示例。
如果我们想要通过一个外部组件(例如按钮)来控制下拉菜单的打开和关闭,该如何处理呢?因为外部组件无法直接访问内部的`setIsComponentVisible`函数。
对于这个问题,我们可以通过将`setIsComponentVisible`函数作为参数传递给外部组件来解决。例如,我们可以定义一个`ToggleButton`组件,将`setIsComponentVisible`作为props传递给它:
const ToggleButton = ({ setIsComponentVisible }) => { const handleClick = () => { setIsComponentVisible(prev => !prev); }; return ( ); } const DropDown = () => { const { ref, isComponentVisible, setIsComponentVisible } = useComponentVisible(true); return (); }{isComponentVisible && ( Going into Hiding
)}
在上述代码中,我们通过`setIsComponentVisible`函数将组件内部的状态和控制函数传递给了`ToggleButton`组件。当点击按钮时,我们可以通过调用`setIsComponentVisible`函数来切换下拉菜单的显示状态。
至于为什么要在`capturing`阶段而不是在通常的`bubbling`阶段设置事件处理程序,这取决于具体的需求和设计。在某些情况下,如果事件在冒泡过程中被阻止或处理,可能会导致意外的行为。通过在`capturing`阶段处理事件,可以更好地控制事件的触发顺序和行为。