-
-
Save gragland/81a678775c30edfdbb224243fc0d1ec4 to your computer and use it in GitHub Desktop.
import { useState, useEffect, useRef } from 'react'; | |
// Usage | |
function App() { | |
// Create a ref that we add to the element for which we want to detect outside clicks | |
const ref = useRef(); | |
// State for our modal | |
const [isModalOpen, setModalOpen] = useState(false); | |
// Call hook passing in the ref and a function to call on outside click | |
useOnClickOutside(ref, () => setModalOpen(false)); | |
return ( | |
<div> | |
{isModalOpen ? ( | |
<div ref={ref}> | |
👋 Hey, I'm a modal. Click anywhere outside of me to close. | |
</div> | |
) : ( | |
<button onClick={() => setModalOpen(true)}>Open Modal</button> | |
)} | |
</div> | |
); | |
} | |
// Hook | |
function useOnClickOutside(ref, handler) { | |
useEffect( | |
() => { | |
const listener = event => { | |
// Do nothing if clicking ref's element or descendent elements | |
if (!ref.current || ref.current.contains(event.target)) { | |
return; | |
} | |
handler(event); | |
}; | |
document.addEventListener('mousedown', listener); | |
document.addEventListener('touchstart', listener); | |
return () => { | |
document.removeEventListener('mousedown', listener); | |
document.removeEventListener('touchstart', listener); | |
}; | |
}, | |
// Add ref and handler to effect dependencies | |
// It's worth noting that because passed in handler is a new ... | |
// ... function on every render that will cause this effect ... | |
// ... callback/cleanup to run every render. It's not a big deal ... | |
// ... but to optimize you can wrap handler in useCallback before ... | |
// ... passing it into this hook. | |
[ref, handler] | |
); | |
} |
Why not click
event?
I am using the hook on a menu button. The button toggles a dropdown and when the dropdown is visible, I use this hook as a acessibility so the user can click outside the dropdown it toggles off too. the problem is that the button itself makes the dropdown toggles twice: off and on.
@notnishi if you'd like to make the button do nothing when the menu is open you could try:
const buttonRef = useRef();
const menuRef = useRef();
useOnClickOutside(menuRef, event => {
if (!buttonRef.current.contains(event.target)) {
setMenuOpen(false);
}
});
Another option would be to add menuRef to an element that wraps both the button and menu. That way clicking the button doesn't trigger the hook handler function and you can just have the button onClick event close the menu itself.
Finally, if that's not an option, you could try react-cool-onclickoutside, which supports adding a special className to elements to prevent them from triggering the hook handler.
I am getting: Uncaught TypeError: ref.current.contains is not a function
@ianobermiller @DimitarNestorov @epzilla @MatxBerg Thanks, I've added
ref
andhandler
to the effect deps. I think that makes sense. Running callback/cleanup on every render is not a big deal and like you say, user can always wraphandler
inuseCallback
to optimize.