Why Custom Hooks?
Ever since Hooks were introduced in React in its v16.8, people started falling in love by using it to build buttery smooth UI with less and readable code.
So did I.
Hooks made our lives easier than anything else ever did.
These are the most used React hooks these days.
But do you know we can make custom hooks ourselves.
Now what are custom hooks?
Custom hooks is a fancy word of quoting a normal function. But the key aspect where custom hooks is differentiated from normal functions is, custom hooks internally uses React hooks to make the functionality happen.
I hope you got a gist of how it works. If you didn't just stick around till the end to understand the intention on when to use it when to not.
We will take a real life example of YouTube thumbnails to get a closer understanding
If you look at here closely, I first clicked on the dropdown of first video, then immediately clicked on the options of second video and as a result the first one's options got closed. Do you know even this is a feature.
We generally try to toggle it on and off only on click of the option menu button, not outside of it.
Just like this below example which I created
Code for below implementation is here
Ewwww!!!
const VideoCard = ({ video }) => {
const { videoId, videoTitle, videoThumbnail } = video;
const [showOptions, setShowOptions] = React.useState(false);
return (
<div key={videoId} className="video__card">
<img src={videoThumbnail} alt="thumbnail1" />
<div className="video_card__bottom">
<p>{videoTitle}</p>
<section className="video__actions">
{/* Toggling the boolean was just not enough here */}
<button onClick={() => setShowOptions((state) => !state)}>
⋮
</button>
<section
className={`video__actions__options ${!showOptions && 'hidden'} `}
>
{/* another fork */}
<span>Like</span>
<span>Add To Playlist</span>
<span>Add To Watch Later</span>
</section>
</section>
</div>
</div>
);
};
We could use a function for the click handler, and check for the element where we click in the dom. If that element doesn't match the options element then toggle it off. But it would be hard to make it reusable as this situation might occur quite often at many places.
All this won't matter in React Styling frameworks as this would be handled and abstracted within.
But Rockstar Developer is who figures out about the abstractions. ๐
We've talked way too much. So you know what's next if you know me :P
Talk is cheap, show me the code - Linus Torvalds
Let's create a custom hook for this problem
Instead of the below useState
hook to toggle the element
const [showOptions, setShowOptions] = useState(false)
Implementation of Custom Hook
Lets create something like this
const {ref, isComponentVisible, setIsComponentVisible } = useClickOutside(false)
But wait, what is this. Lets breakdown
- ref : You want to create a
ref
usinguseRef
to detect the element from DOM which you are clicking on - isComponentVisible and setIsComponentVisible : You might have guessed these are just state variables and yes they are and it takes the initial value as
false
because the element would be hidden obviously. - useClickOutside : The hook we are going to implement.
const useClickOutside = (initialVisibleState = false) => {
const ref = useRef(null);
const [isComponentVisible, setIsComponentVisible] = useState(initialVisibleState)
{/* Here goes the actual implementation */}
{/* These are the values that we use to set or refer while using the hook as shown in the above code snippet */}
return {ref, isComponentVisible, setIsComponentVisible};
}
Now lets talk a little more:
Look at the cases which we need to deal:
- The component should open on click of options menu.
- The component should close on click of the same options menu.
The component should be closed on click of anywhere outside the it.
Add a handler for it in the hook
const handleClickOutside = (event) => {
{/* Check first whether the `ref` is valid or not, and next check whether the `ref` is not pointing to the actual button or buton's parent */}
{/* If so then hide the component , or keep it the same*/}
if (ref.current && !ref.current.contains(event.target)) {
setIsComponentVisible(false);
}
}
Thats it.
But wait its not working.
There is a little more to do and you'll be good to go. That is, to use this handler effectively everytime.
useEffect(() => {
{/*The handler event occurs on click of that ref element, and run the event in capturing phase of parent to child hierarchy */}
document.addEventListener('click', handleClickOutside, true);
{/* Do the same in the cleanup function as the unmounted part also should be handled */}
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, []);
Boom! You have your custom hook that internally uses 3 React Hooks ready.
Now lets use this in out App.
import React from 'react';
import useClickOutside from './useClickOutside';
const VideoCard = ({ video }) => {
const { videoId, videoTitle, videoThumbnail } = video;
const { ref, isComponentVisible, setIsComponentVisible } =
useClickOutside(false);
return (
<div key={videoId} className="video__card">
<img src={videoThumbnail} alt="thumbnail1" />
<div className="video_card__bottom">
<p>{videoTitle}</p>
<section ref={ref} className="video__actions">
<button onClick={() => setIsComponentVisible((state) => !state)}>
⋮
</button>
<section
className={`video__actions__options ${
!isComponentVisible && 'hidden'
} `}
>
{/* another fork */}
<span>Like</span>
<span>Add To Playlist</span>
<span>Add To Watch Later</span>
</section>
</section>
</div>
</div>
);
};
export { VideoCard };
See the working example now.
Let's take it a notch higher by making small refactoring
- What if your dropdown action menu is totally another component and you need to take
ref
from that. Usingref
prop on a component won't work normally asref
only works on HTML elements to take the reference from. - But there is a way to forward that
ref
and that's whereforwardRef
comes up.
This is how your code will change from previous one to this one
forwardRef takes a callback function as an argument
the callback function takes two arguments. One is the props passed to the component, the other is the
ref
passed to the component. Inside the callback function lies your JSX.
##Completed implementing Custom Hook with best practices
- You created a custom hook.
- It was used from parent component to child component using forwardRef and clean code.
- Key takeaway . Only create custom hooks if the problem/feature dependency lies in more than one component, otherwise would make sense creating such amount of code.
Thanks for the read. Would appreciate your feedback and suggestions over the information that you got from the blog. ๐๐๐