React 下拉菜单只切换一次

React dropdown toggles only once

我正在尝试使用 useRef 钩子和 Typescript 构建 React Dropdown 组件: 如果我单击一次切换按钮或在其外部单击,它会正确打开并关闭,但是当我想再次打开它时它会关闭。有任何想法吗 ?这是我以某种方式丢失了 ref 引用吗?

用法如下:

https://codesandbox.io/s/react-typescript-obdgs

import React, { useState, useRef, useEffect } from 'react'
import styled from 'styled-components'

interface Props {}

const DropdownMenu: React.FC<Props> = ({ children }) => {
  const [menuOpen, toggleMenu] = useState<boolean>(false)
  const menuContent = useRef<HTMLDivElement>(null)

  useEffect(() => {
    // console.log(menuOpen)
  }, [menuOpen])

  const showMenu = (event: React.MouseEvent) => {
    event.preventDefault()
    toggleMenu(true)
    document.addEventListener('click', closeMenu)
  }

  const closeMenu = (event: MouseEvent) => {
    const el = event.target
    if (menuContent.current) {
      if (el instanceof Node && !menuContent.current.contains(el)) {
        toggleMenu(false)
        document.removeEventListener('click', closeMenu)
      }
    }
  }

  return (
    <div>
      <button
        onClick={(event: React.MouseEvent) => {
          showMenu(event)
        }}
      >
        Open
      </button>
      {menuOpen ? <div ref={menuContent}>{children}</div> : null}
    </div>
  )
}

export default DropdownMenu

问题出在您的 onClick 按钮上。每次单击按钮时,您都在调用 showMenu,因此您每次都在添加新的事件侦听器。

如果菜单已经显示,您不想调用 showMenu,因此修复可以是:

<button onClick={(event: React.MouseEvent) => {
  if (!menuOpen) showMenu(event);
}}>

如果您点击该按钮两次,您将无法再次打开它。如果您在按钮外部单击以关闭,它将按预期工作。

这可能是因为即使菜单已经显示,您的 showMenu 回调也会执行,这会导致附加多个 closeMenu 事件侦听器,进而导致奇怪的行为。

closeMenu 事件侦听器应在效果内创建,而不是在 showMenu 回调中创建。

const showMenu = (event: React.MouseEvent) => {
    event.preventDefault()
    toggleMenu(true)
}

// closeMenu is the same
const closeMenu = (event: MouseEvent) => {
    const el = event.target
    if (menuContent.current) {
        if (el instanceof Node && !menuContent.current.contains(el)) {
            toggleMenu(false)
            document.removeEventListener('click', closeMenu)
        }
    }
}

useEffect(() => {
    if (!menuOpen) {
        return
    }
    document.addEventListener('click', closeMenu)
    return () => {
        document.removeEventListener('click', closeMenu)
    }
}, [menuOpen])

useEffect 真的很酷 - 移除事件侦听器的返回函数将在更改 menuOpen 时和卸载组件时调用。在您之前的代码中,如果卸载组件,则不会删除事件侦听器。