Easily Detect Outside Click Using useRef Hook

Let's see how to detect a click outside of an element using useRef.

Easily Detect Outside Click Using useRef Hook
Related Posts

Hello World 👋

Hooks are special types of functions in React that you can call inside React functional components. They let you store data, add interactivity, and perform some actions, otherwise known as side-effects.
The most common hooks are:
  • useState
  • useEffect
  • useRef
  • useContext
  • useReducer
In the previous article (How to Create a Reusable LocalStorage Hook), we learned about useEffect hook and how we can use it to create a custom and reusable hook that persists the state by storing it in local storage. If you haven’t read that article, please go and read it before going through this article. We will be using useEffect in this article.

useRef

This is a special inbuilt function in React that gives you a direct reference to DOM node. Usually, in React, you won’t have access to the DOM nodes directly. But sometimes, you may want to get access to DOM nodes directly because of various reasons, like the library that you use may need that.
useRef takes a single argument which is the initial value for the ref and creates and returns a ref.
const elementRef = useRef(null)
Now, the way to ask React to give you the access to DOM node is to assign the created ref to the ref prop of the element in JSX.
For example,
function HelloWorld() {
    // create the ref    
    const elementRef = useRef(null) 
		return (
					{/* Asking React for the access to the DOM node */}
        <>            
					<div ref={elementRef}>
							Hello World
					</div>        
				</>
		)
}
Now, when you add the ref prop for the JSX element, React understands that you want direct reference to the DOM node of that element, and then it sets the current property of that elementRef to the DOM node.
In the above example, you can access the DOM node by using elementRef.current

Detect Click Outside

Let’s use this to detect whenever you click outside of an element.
Some of the practical use-cases where you may want to detect if you clicked outside of an element are:
  • When you have a modal(popup/dialog), and you want to close the modal whenever you click outside of it.
  • When you have a dropdown, and you want to close it whenever you click outside of it.
function App() {
    const [isOpen, setIsOpen] = useState(true)
    return (
				<>
					<div>
						<h2>App with a Modal</h2>
						<button onClick={() => setIsOpen(true)}>
								Open Modal
						</button>
						<div id="modal">
							<Modal isOpen={isOpen}>
								This is the modal dialog
							</Modal>
						</div>        
				</>
		)
}
Let’s take this simple component. It has a heading, a button which when clicked opens the modal.
Our goal is to detect and execute setIsOpen(false) whenever we click outside of div with id modal.
Let’s see how we can achieve this. 1. We need a reference to the div with id modal. 1. We need to detect a click. 1. We need to see if the click happened outside of the modal div. 1. Then we need to execute setIsOpen(false)

Step 1: Getting a reference to Modal

We can use useRef for this.
function App() {
    const [isOpen, setIsOpen] = useState(true)
    // change starts here
    const modalRef = useRef()
    // change ends here
    return (
        <>
            <div>
                <h2>App with a Modal</h2>
                <button onClick={() => setIsOpen(true)}>Open Modal</button>
                {/* <!-- Change starts here --> */}
                <div id="modal" ref={modalRef}>
                {/* <!-- Change ends here --> */}
                    <Modal isOpen={isOpen}>
                        This is the modal dialog
                    </Modal>
                </div>
        </>
    )
}
Now, after the app gets rendered, modalRef.current will have access to the required DOM node.

Step 2. Add a click event listener

We can add an event listener inside useEffect.
useEffect(() => {
    function handler(event) {
        console.log(event, 'clicked somewhere')
    }
    window.addEventListener('click', handler)
    return () => window.removeEventListener('click', handler)
}, [])
Here we added a click event listener to the entire window to detect the click anywhere on the window.

Step 3: Detect if the click happened outside of the window

We can know where the click happened based on event.target. We just have to check if our modal div contains event.target or not.
useEffect(() => {
    function handler(event) {
        // change starts here
        if(!modalRef.current?.contains(event.target)) {
            console.log('clicked outside of modal')
        }
        // change starts here
    }
    window.addEventListener('click', handler)
    return () => window.removeEventListener('click', handler)
}, [])

Step 4: Close the modal whenever you click outside of modal

This step is straight-forward. We just have to execute setIsOpen(false) whenever we detect the click outside the modal.
useEffect(() => {
    function handler(event) {
        if(!modalRef.current?.contains(event.target)) {
            // change starts here
            setIsOpen(false)
            // change starts here
        }
    }
    window.addEventListener('click', handler)
    return () => window.removeEventListener('click', handler)
}, [])
Let’s put everything together.
function App() {
    const [isOpen, setIsOpen] = useState(true)
    const modalRef = useRef()

    useEffect(() => {
        function handler(event) {
            if(!modalRef.current?.contains(event.target)) {
                setIsOpen(false)
            }
        }
        window.addEventListener('click', handler)
        return () => window.removeEventListener('click', handler)
    }, [])

    return (
        <>
            <div>
                <h2>App with a Modal</h2>
                <button onClick={() => setIsOpen(true)}>Open Modal</button>
                <div id="modal" ref={modalRef}>
                    <Modal isOpen={isOpen}>
                        This is the modal dialog
                    </Modal>
                </div>
        </>
    )
}

Creating a reusable hook

We can create a reusable hook out of this that you can use anywhere.
function App() {
    const [isOpen, setIsOpen] = useState(true) 
		const modalRef = useRef() 
		
		useEffect(() => {
        function handler(event) {
            if (!modalRef.current?.contains(event.target)) {
                setIsOpen(false)
            }
        }
        window.addEventListener('click', handler) 
				return () => window.removeEventListener('click', handler)
    }, []) 
		
			return (
					<>
						<div>
							<h2>App with a Modal</h2>
							<button onClick={() => setIsOpen(true)}>
								Open Modal
							</button>
							<div id="modal" ref={modalRef}>
								<Modal isOpen={isOpen}>
									This is the modal dialog
								</Modal>
							</div>
					</>
			)
}
In this hook, we are creating a ref and then returning it at the end. This way, the API looks kinda similar to how you create a ref using useRef. But the ref created using this custom hook has the additional functionality to detect and execute a callback whenever a click is detected outside.
Let’s change our example to use this hook.
function App() {
    const [isOpen, setIsOpen] = useState(true)
    const modalRef = useOnClickOutsideRef(() => setIsOpen(false))

    return (
        <>
            <div>
                <h2>App with a Modal</h2>
                <button onClick={() => setIsOpen(true)}>Open Modal</button>
                <div id="modal" ref={modalRef}>
                    <Modal isOpen={isOpen}>
                        This is the modal dialog
                    </Modal>
                </div>
        </>
    )
}
That’s it. You now have the exact same functionality as you have before. The only thing you changed here is changing useRef() to useOnClickOutsideRef(() => setIsOpen(false)).
Accessing DOM nodes is not the only case when you can use ref. You can use ref to keep a reference to any value. You can even mutate the ref directly using exampleRef.current = 'something'. Mutating the ref will not cause the component to re-render. So, whenever you want to keep track of a value and want to mutate it without causing the component to re-render, you can make use of useRef hook.
To know a practical use-case that uses useRef to keep track of a value, check out this excellent article by @Tapas Adhikary - How to use JavaScript scheduling methods with React hooks

What have you learned?

  • useRef Hook
    • It is used to create refs. It takes the initial value of ref as a single argument.
    • When you assign the ref (created using useRef hook) to the ref property of JSX element, React automatically sets the current property of that ref to the DOM node of the corresponding element.
    • You can mutate the ref.current property directly and mutating it does not cause the component to re-render.
  • We also learned how to create a useOnClickOutsideRef using useRef and useEffect - which can detect and execute a callback whenever you clicked outside of an element.

What’s Next?

In the next article, we will look at the hooks flow to see in which order different hooks will get executed. We will also see what lifting state and colocating state mean and when to use each of them.

Until Next Time 👋

If you liked this article, check out
 
Bhanu Teja P

Written by

Bhanu Teja P

24yo developer and blogger. I quit my software dev job to make it as an independent maker. I write about bootsrapping Feather.