I am trying to build React Dropdown component using useRef hook and Typescript: It opens correctly and closes if I click toggle button once or click outside of it, but it will closed when I want to open it again. Any ideas ? Is this I am loosing ref referance somehow ?
Here is usage:
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
If you click the button twice, you will not be able to open it again. If you click outside the button to close, it will work as expected.
This is probably because your showMenu callback is executed even when the menu is already shown, which results in multiple closeMenu event listeners being attached, which in turn leads to weird behaviour.
The closeMenu event listener should be created inside an effect, not in the showMenu callback.
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 is really cool - the returned function where the event listener is removed will be called both when menuOpen is changed, and when the component is unmounted. In your previous code, if the component would be unmounted, the event listener would not be removed.