Выпадающее меню React переключается только один раз

Я пытаюсь создать компонент React Dropdown с помощью хука useRef и Typescript: он открывается правильно и закрывается, если я нажимаю кнопку переключения один раз или щелкаю за его пределами, но он закроется, когда я захочу открыть его снова. Есть идеи? Я как-то теряю референцию?

Вот использование:

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

2 ответа

Решение

Если вы нажмете кнопку дважды, вы не сможете открыть ее снова. Если вы щелкнете за пределами кнопки, чтобы закрыть, она будет работать должным образом.

Вероятно, это связано с тем, что ваш обратный вызов 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, так и при размонтировании компонента. В предыдущем коде, если компонент был бы отключен, прослушиватель событий не был бы удален.

Проблема возникает из-за вашего onClick на кнопке. Вы звоните,showMenu каждый раз, когда вы нажимаете кнопку, поэтому вы каждый раз добавляете новый прослушиватель событий.

Ты не хочешь звонить showMenu если меню уже отображается, исправление может быть следующим:

<button onClick={(event: React.MouseEvent) => {
  if (!menuOpen) showMenu(event);
}}>
Другие вопросы по тегам