Instantly share code, notes, and snippets.

Embed
What would you like to do?
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);
};
}, []); // Empty array ensures that effect is only run on mount and unmount
}
@Aulos

This comment has been minimized.

Copy link

Aulos commented Nov 6, 2018

Why not call useRef inside of useOnClickOutside? https://gist.github.com/Aulos/5ad4d9f5d030ac857f57125e7a407d99

@gragland

This comment has been minimized.

Copy link
Owner

gragland commented Nov 6, 2018

@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.

@ianobermiller

This comment has been minimized.

Copy link

ianobermiller commented Nov 9, 2018

You may want to pass handler on line 44, otherwise it will be calling the old version if the function ever changes (like if it depends on other state, for example). Then you will also have to make sure it is memoized going into useOnClickOutside: https://gist.github.com/ianobermiller/653cee488fb05a06343cd8a2b5d2b176

@Andarist

This comment has been minimized.

Copy link

Andarist commented Nov 10, 2018

@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

@gragland

This comment has been minimized.

Copy link
Owner

gragland commented Nov 20, 2018

@ianobermiller @Andarist: Good feedback! Any reason why I can't just memoize the handler within useOnClickOutside so that it doesn't matter whether they memoize?

@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

@j-f1

This comment has been minimized.

Copy link

j-f1 commented Jan 9, 2019

Why not return the ref from useOnClickOutside?

@gragland

This comment has been minimized.

Copy link
Owner

gragland commented Jan 9, 2019

@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.

@MatxBerg

This comment has been minimized.

Copy link

MatxBerg commented Jan 22, 2019

I had to remove the [] observer to make sure the handler can use local state from component when called.

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