Easily Detect Outside Click Using useRef Hook in React
Tags: react, javascript, webdev
Published: 2020-11-03 05:01:20

## 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](https://blog.bhanuteja.dev/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.

```js
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,

```js
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.

```js
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 reference to Modal
We can use `useRef` for this.

```jsx
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`.

```jsx
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.

```jsx
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.

```jsx
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.
```js
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.
```js
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
}
```
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.
```jsx
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.

## 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
- [How to Create a Reusable LocalStorage Hook](https://blog.bhanuteja.dev/how-to-create-a-reusable-localstorage-hook)
- [React Hooks: Managing State With useState Hook](https://blog.bhanuteja.dev/react-hooks-managing-state-with-usestate-hook)

You can also follow me on Twitter at [@pbteja1998](https://twitter.com/pbteja1998).