Skip to content

Instantly share code, notes, and snippets.

@Phryxia
Last active May 12, 2025 17:56
Show Gist options
  • Select an option

  • Save Phryxia/2b3f884345261fbd19b95505ced8eabc to your computer and use it in GitHub Desktop.

Select an option

Save Phryxia/2b3f884345261fbd19b95505ced8eabc to your computer and use it in GitHub Desktop.
Simple react hook implementation for detecting click of outside of the given DOM
import { useCallback, useRef } from 'react'
type MouseEventHandler = (e: MouseEvent) => void
export function useOutsideClickHandler<T extends HTMLElement>(
callback: MouseEventHandler,
) {
const userCallback = useRef<MouseEventHandler>(() => {})
userCallback.current = callback
const handlerPair = useRef<MouseEventHandler>(() => {})
const refInitializer = useCallback((dom: T | null) => {
if (dom) {
handlerPair.current = (e: MouseEvent) => {
if (e.target instanceof Node && !dom.contains(e.target)) {
userCallback.current(e)
}
}
window.addEventListener('click', handlerPair.current)
} else {
window.removeEventListener('click', handlerPair.current)
}
}, [])
return refInitializer
}
@Phryxia
Copy link
Author

Phryxia commented Apr 10, 2022

Example

function Component() {
  const [isModalOpen, setIsModalOpen] = useState(false)
  const modalRef = useOutsideClickHandler<HTMLDivElement>(() => setIsModalOpen(true))
  
  return (
    <div>
      <button onClick={() => setIsModalOpen(true)}>Open</button>
      {isModalOpen && <div ref={modalRef}>I'm Modal</div>}
    </div>
  )
}

Pitfall

This doesn't consider actual visual intersection, rather it only takes account of HTML's hierarchy.

Patch Notes

  • 2025-05-13
    • Return function for ref props instead of LegacyRef to handle mount/demount more robustly.
    • Check e.target being instance of Node explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment