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] | |
); | |
} |
This comment has been minimized.
This comment has been minimized.
@Aulos The only problem with that is if you need to have multiple hooks access the same ref. You'd want to be able to pass the ref in as an argument. That said, if I know I never need to do that in my codebase I'd probably do it your way. |
This comment has been minimized.
This comment has been minimized.
You may want to pass |
This comment has been minimized.
This comment has been minimized.
@ianobermiller that's the risk, but IMHO it's better choice to use "first handler" only rather than dispose previous effect and setup it again on each render as people most commonly will be using inline handlers with this and won't memoize them. the implementation doesn't account for passive events, you could also link somewhere to existing libraries doing the same thing (I actually have created this one some days ago https://github.com/Andarist/use-onclickoutside |
This comment has been minimized.
This comment has been minimized.
@ianobermiller @Andarist: Good feedback! Any reason why I can't just memoize the handler within @Andarist: I updated the post to link to your library with a mention that your's accounts for passive events (posts now have an "also check out" section at the bottom): https://usehooks.com/#useOnClickOutside |
This comment has been minimized.
This comment has been minimized.
Why not return the ref from |
This comment has been minimized.
This comment has been minimized.
@j-f1 The problem with that is if you need to have multiple hooks access the same ref. You'd want to be able to pass the ref in as an argument. |
This comment has been minimized.
This comment has been minimized.
I had to remove the [] observer to make sure the handler can use local state from component when called. |
This comment has been minimized.
This comment has been minimized.
EDIT: Actually that would be the same as removing the dependencies. Maybe leave EDIT: Or we actually do put |
This comment has been minimized.
This comment has been minimized.
@MatxBerg @dimitarnestorov I like the idea of making an optional |
This comment has been minimized.
This comment has been minimized.
@ianobermiller @dimitarnestorov @epzilla @MatxBerg Thanks, I've added |
This comment has been minimized.
This comment has been minimized.
Why not |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
@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. |
This comment has been minimized.
Why not call
useRef
inside ofuseOnClickOutside
? https://gist.github.com/Aulos/5ad4d9f5d030ac857f57125e7a407d99