Implementing Custom Hooks 🔥 useHover(), useFocus() & useClickOutside()
UseHover()
It is common to see conditional rendering based on the hover state of some element. We can achive it by the CSS pseduo class :hover
, but for more complex cases it might be better to have a state controlled by script.
lets create useHover() to support this
import React, { useState, useRef, useEffect, useCalback } from 'react';
export function useHover() {
const ref = useRef(null);
const [isHovered, setIsHovered] = useState(false);
const handleMouseHover = useCallback(() => {
setIsHovered(!isHovered)
}, [isHovered]);
useEffect(() => {
const element = ref.current
if (element) {
element.addEventListener('mouseenter', handleMouseHover);
element.addEventListener('mouseleave', handleMouseHover);
}
return () => { // callback/cleanup to run every render. It's not a big deal ...
element.removeEventListener('mouseenter', handleMouseHover);
element.removeEventListener('mouseleave', handleMouseHover);
}
})// ... function on every render that will cause this effect ...
return [ref, isHovered];
}
and Usage:
function App() {
const [ref, isHovered] = useHover()
return (
<div ref={ref}>
{isHovered ? 'hovered' : 'not hovered'}
</div>
)
}
useFocus()
CSS pseudo-class :focus-within could be used to allow conditional rendering in parent element on the focus state of descendant elements.
While it is cool, in complex web apps, it might be better to control the state in script.
Now lets create useFocus()
to support this.
export function useFocus() {
const ref = useRef(null)
const [isFocused, setIsFocused] = useState(false)
const toggle = useCallback(() => {
setIsFocused(!isFocused)
}, [isFocused])
useEffect(() => {
const element = ref.current
element?.addEventListener('focus', toggle)
element?.addEventListener('blur', toggle)
return () => {// callback/cleanup to run every render. It's not a big deal ...
element?.removeEventListener('focus', toggle)
element?.removeEventListener('blur', toggle)
}
}) // ... function on every render that will cause this effect ...
return [ref, isFocused]
}
and usage:
function App() {
const [ref, isFocused] = useFocus()
return <div>
<input ref={ref}/>
{isFocused && <p>focused</p>}
</div>
}
useClickOutside()
This hook allows you to detect clicks outside of a specified element. In the example below we use it to close a modal when any element outside of the modal is clicked. By abstracting this logic out into a hook we can easily use it across all of our components that need this kind of functionality (dropdown menus, tooltips, etc).
import { useState, useEffect, useRef } from "react";
function useOnClickOutside(callback) {
const ref = useRef(null)
useEffect(() => {
const listener = (event) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) {
return;
}
callback(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {// callback/cleanup to run every render. It's not a big deal ...
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, []);
return ref
}
usage
// Usage
function App() {
const [isModalOpen, setModalOpen] = useState(false);
// Call hook passing a function to call on outside click
const ref = useOnClickOutside(() => setModalOpen(false));
return (
<div>
{isModalOpen ? (
<div ref={ref}>
👋 Hey, I'm a modal. Click anywhere outside of me to close.
</div>
) : (
<button onClick={() => setModalOpen(true)}>Open Modal</button>
)}
</div>
);
}
Thanks for reading and Have a great day! ☺
I wrote another article about usePrevious() and useTimeOut() hooks, you can find it here
Follow me to see more relevant content 😍
References
- Questions are from https://bigfrontend.dev/react
- https://usehooks.com/useOnClickOutside/