使用hooks在React组件外部检测点击事件

16 浏览
0 Comments

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

0
0 Comments

在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 (
    
{inner && children}
) } 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 }

最后,我们可以在其他组件中使用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)

0
0 Comments

在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`阶段处理事件,可以更好地控制事件的触发顺序和行为。

0