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.
- We need a reference to the div with id
modal
. - We need to detect a click.
- We need to see if the click happened outside of the modal div.
- 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>
</>
)
}
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.
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.
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 usinguseRef
hook) to theref
property of JSX element, React automatically sets thecurrent
property of thatref
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
usinguseRef
anduseEffect
- 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.
Top comments (3)
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:
In the
useOnClickOutsideRef
hook, the passed callback is used as a dependency of the effect:Thus, the
window.click
is unmounted and mounted again each time the component that callsuseOnClickOutsideRef
is rendered.Providing the
useMemo
-wrapped callback ensures the same function is passed down the hook:Yes, you are right. I missed that.
Really nice article, thanks!