DEV Community

loading...
Cover image for Easily Detect Outside Click Using useRef Hook in React

Easily Detect Outside Click Using useRef Hook in React

Bhanu Teja Pachipulusu
Full-Stack Developer and Co-Founder at Coderplex
Originally published at blog.bhanuteja.dev ・5 min read

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)
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
}

Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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.
  2. We need to detect a click.
  3. We need to see if the click happened outside of the modal div.
  4. Then we need to execute setIsOpen(false)

Step 1: Getting 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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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)
}, [])
Enter fullscreen mode Exit fullscreen mode

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)
}, [])
Enter fullscreen mode Exit fullscreen mode

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)
}, [])
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

Creating a reusable hook

We can create a reusable hook out of this that you can use anywhere.

import { useEffect, useRef } from 'react'

export default function useOnClickOutsideRef(callback, initialValue = null) {
  const elementRef = useRef(initialValue)
  useEffect(() => {
    function handler(event) {
      if (!elementRef.current?.contains(event.target)) {
        callback()
      }
    }
    window.addEventListener('click', handler)
    return () => window.removeEventListener('click', handler)
  }, [callback])
  return elementRef
}
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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.

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 corresponsing 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

You can also follow me on Twitter at @pbteja1998.

Discussion (3)

Collapse
milichev profile image
Vadym Milichev

Neat and refreshing approach! :)

Please confirm if the following makes sense:

In the App component, a new instance of the callback function is created on each render:

    const modalRef = useOnClickOutsideRef(() => setIsOpen(false))
Enter fullscreen mode Exit fullscreen mode

In the useOnClickOutsideRef hook, the passed callback is used as a dependency of the effect:

  }, [callback]) <--
  return elementRef
}
Enter fullscreen mode Exit fullscreen mode

Thus, the window.click is unmounted and mounted again each time the component that calls useOnClickOutsideRef is rendered.

Providing the useMemo-wrapped callback ensures the same function is passed down the hook:

// App
    const modalRef = useOnClickOutsideRef(useMemo(() => setIsOpen(false), []));
Enter fullscreen mode Exit fullscreen mode
Collapse
pbteja1998 profile image
Bhanu Teja Pachipulusu Author

Yes, you are right. I missed that.

Collapse
nans profile image
Nans Dumortier

Really nice article, thanks!