In this post, we are going to see how to create our own React Hook in just a few minutes. If you don't know what is a React Hook, I suggest you first read my post about it: The BEST REACT Concept (NO CLASS NEEDED).
If you are using a lot of event listeners with the Effect hook, useEffect
, you should consider moving that logic to a custom hook. In this tutorial, we are going to create our own hook to handle those event listeners, named useEventListener
.
If you'd like to see it in action, check the CodeSandbox demo.
Building our Custom Hook
Do you remember? React hooks are just functions! That's what makes them so special. Really! They are just like normal functions. We can decide what the hook takes in as arguments, and what it returns.
The only requirement is that those functions should have a name starting with use. This way it is easy to tell they are React hooks.
"Got it. Shall we start writing some code?"
Sure! As I said, let's name our React hook useEventListener
.
// Our own Hook
function useEventListener(){
...
}
This is just a function. Nothing more.
So, let's add the arguments to our function. It should accept the event name we should listen to, the event handler to fire, and the element to add the event listener onto.
// Our own Hook
function useEventListener(eventName, eventHandler = () => null, element = window){
...
}
"Then, what should we do?"
Now, we should use the Effect hook, useEffect
, to add our event listener logic.
useEffect(() => {}, []);
First things first. Let's make sure that our element supports the addEventListener
method. If not, we return without doing anything else.
useEffect(
() => {
// Check if the element supports the addEventListener method
const checked = element && element.addEventListener;
// Stop here if not supported
if (!checked) return;
...
);
Then, let's add the event listener onto our element.
...
// Add event listener
element.addEventListener(eventName, handleEventHandler);
...
"Hey wait! Where does the
handleEventHandler
method come from?"
Bare with me. We'll talk about it shortly.
Finally, the function passed to the useEffect
hook should return a clean-up function. Indeed, before the component leaves the screen, we should clean-up our mess by removing the registred event listener. Let's do this.
...
// Remove event listener on cleanup
return () => {
element.removeEventListener(eventName, handleEventHandler);
};
...
"Are we done now with the
useEffect
hook?"
Actually, there is one more thing. Indeed, we need to tell React to run our effect only if the eventName
, the element
, or the handleEventHandler
changes. We can do this by passing them in the dependencies array.
useEffect(
() => {
// Check if the element supports the addEventListener method
const checked = element && element.addEventListener;
// Stop here if not supported
if (!checked) return;
// Add event listener
element.addEventListener(eventName, handleEventHandler);
// Remove event listener on cleanup
return () => {
element.removeEventListener(eventName, handleEventHandler);
};
},
[eventName, element, handleEventHandler] // Re-run if eventName, element, or eventHandler changes
);
1 minute left and we should be done with our React hook. Let's go back to the handleEventHandler
method. This is the method we used as the callback to the addEventListener
method.
This method should run our eventHandler
in arguments. To implement it, we'll use the useCallback
hook in order to return a memoized version of our eventHandler
. This way it will only change if the eventHandler
has changed. It allows us to prevent unnecessary renders.
import { useEffect, useCallback } from "react";
...
const handleEventHandler = useCallback(
event => {
if (typeof eventHandler === "function") {
eventHandler(event);
}
},
[eventHandler]
);
...
That's it! Now, you know how to create your own React hook.
"Awesome! But, could you also please show me how to use it?"
Using our Custom Hook
Sure! Let's see how to use our useEventListener
hook. I've got the perfect use case for that. I've built the following Modal
component and I want to listen for click event to close the modal if the user clicks outside the component.
I have omitted the styles for readability. Check the CodeSandbox demo for that.
import React from "react";
const Modal = ({ show = false, onClose = () => null, children }) => {
return (
show && (
<div className="modal-container">
<div className="modal-content center">
{children}
</div>
</div>
)
);
};
export default Modal;
To listen for the user clicks, we will call our useEventListener
hook with the mousedown
event name and pass an event handler function named handleOnClickOutside
.
const Modal = ({ show = false, onClose = () => null, children }) => {
const handleOnClickOutside = event => {
...
};
// Use our custom hook to listen for mouse down event
useEventListener("mousedown", handleOnClickOutside);
return (
show && (
<div className="modal-container">
<div className="modal-content center">
{children}
</div>
</div>
)
);
};
This is in the handleOnClickOutside
function that we are going to check if the user clicked inside or outside the component. If yes, we will run the onClose
function passed in argument to the Modal
. Otherwise, we do nothing.
To check that, we need to use the useRef
hook in order to access the DOM node of our Modal
.
Here's how our final component should look like.
import React, { useRef } from "react";
import useEventListener from "./useEventListener";
const Modal = ({ show = false, onClose = () => null, children }) => {
const modalRef = useRef(null);
const handleOnClickOutside = event => {
if (modalRef.current && !modalRef.current.contains(event.target)) {
onClose();
}
};
// Use our custom hook to listen for mouse down event
useEventListener("mousedown", handleOnClickOutside);
return (
show && (
<div className="modal-container">
<div ref={modalRef} className="modal-content center">
{children}
</div>
</div>
)
);
};
export default Modal;