Detecting double clicks using React hooks
We all have seen the images on mobile devices. As a user, we are used to double click and zoom the images. That gives better experience to users wanting to see the close-ups. It is a must-have functionality on mobile sites using image. E-commerce website are common example where image zooming using double click is a mandatory feature.
But to have the image zoom functionality, we should be able to detect double click first. We are going to see how we can detect double clicks on any DOM element in this article using React custom hook. I am going to discuss image dragging and zooming in follow up articles. So let’s start guys.
The idea is to listen for two consecutive clicks happening in a short period. In our case, we can take that time gap as 200ms. Let’s understand the crux of it, when not implemented using Reactjs.
So we have to time-bound 2 consecutive clicks. Let’s assume the set of actions we need to do in response to a double click is wrapped inside a callback function.
On every click, we need to check whether there was a click in last 200ms.
If there was a click in last 200ms. And with the current click, the total click count becomes 2 :
- Run the callback.
- Reset the click count to 0.
- Clear the timer value (explanation in below points)
If there was not a click in last 200ms before the current click,
- Set a new Timer using setTimeout.
- Set the count value to 1
If no click happened after the first click in 200ms, the setTimeout callback will
- Clear the timeout.
- Reset the click count to 0.
The above mentioned covers all the scenarios needed to implement useDoubleClick hook. Now let’s implement the above-mentioned scenarios in Reactjs. Now first, let’s see the complete code for useDoubleClick hook, and then we will discuss what’s happening line by line.
function useDoubleClick(callback) {
/** callback ref Pattern **/
const [elem, setElem] = React.useState(null)
const callbackRef = React.useCallback(node => {
setElem(node)
callbackRef.current = node
}, [])
const countRef = React.useRef(0)/** Refs for the timer **/
const timerRef = React.useRef(null)/**Input callback Ref for callback passed **/
const inputCallbackRef = React.useRef(null)
React.useEffect(() => {
inputCallbackRef.current = callback
})
return [callbackRef, elem]
}
Let’s understand the code above. useDoubleClick takes our callback as the argument which will be invoked when double click happens.
We are using callback ref pattern which takes away the need to pass the ref argument to it. The line which are responsible for callback ref pattern are:
const [elem, setElem] = React.useState(null)
const callbackRef = React.useCallback(node => {
setElem(node)
callbackRef.current = node
}, [])
The callback ref is returned to the consumer, and the consumer can attach it to the target DOM element. We are using useCallback React official hook.
useCallback takes two arguments. The first one is the callback, and the second one is the dependency array.
useCallback synchronizes the callback with the second argument, which is our dependency array. Let’s see the synchronization rules which applies for most of the official hooks.
- No second argument means the callback is synchronized with every render of component consuming it. The reference to the callback is going to change on every render of the component.
- Blank second argument means the callback is synchronized with nothing, and the reference to the callback is going to change once after the first render of the component consuming it.
- Non-blank second argument means that the callback is synchronized with all the values passed in the array. If anything changes the callback reference is changed
callbackRef is returned from hook, which can be used by consumer to set the ref to target element(element on which double click is needed)
const [refCallback, elem] = useDoubleClick(callback);
return (
<div className="test" ref={refCallback}>
....
.... JSX
</div>
)
We are using useState to keep the reference to the actual DOM element. The elem value is going to get updated with actual target Element when the component has rendered
We also need to do the bookkeeping for click count and timer id as the pseudo code mentioned in the start.
/** useRef used for count **/
const countRef = React.useRef(0)/** useRef used for timer id **/
const timerRef = React.useRef(null)
Now let’s add the crux of the double click hook.
React.useEffect(() => {
function handler() {
const isDoubleClick = countRef.current + 1 === 2
const timerIsPresent = timerRef.current
if (timerIsPresent && isDoubleClick) {
clearTimeout(timerRef.current)
timerRef.current = null
countRef.current = 0
if (inputCallbackRef.current) {
inputCallbackRef.current()
}
}
if (!timerIsPresent) {
countRef.current = countRef.current + 1
const timer = setTimeout(() => {
clearTimeout(timerRef.current)
timerRef.current = null
countRef.current = 0
}, 200)
timerRef.current = timer
}
}
if (elem) {
elem.addEventListener('click', handler)
}return () => {
if (elem) {
elem.removeEventListener('click', handler)
} }
}, [elem])
We are using useEffect React official hook, which follow similar kind of rules which we explained for useCallback.
We are adding an event listener to the target element in callback passed and removing the event listener when the effect re-runs.
The handler function is taking care of all the different states of click count and timer value. Try the code above to the pseudo code we discussed earlier.
Let’s see the complete useDoubleClick hook.
Let see the usage of the this hook
useDouble click is a custom hook that used official hooks to implement custom functionality. With Reactjs hooks, it’s more than ever easy to compose together logic and reuse it across components.
Here is the working link of codesandbox.
When I was thinking of writing this article, I thought of checking the native event listener, if any. Google came up with this :
https://caniuse.com/#feat=mdn-api_element_dblclick_event
The support of ‘dblclick’ event is still not great. And this hook can be of good use if you are doing any Reactjs project which needs double click functionality.
Over to you guys, Please feel free to comment on any problems with the article. Share your concerns in React hooks on twitter. I will try to solve it and explain it to you guys.
Thanks