Preventing click events on double click with React, the performant way
A few months ago I had to deal with a problem: React triggers the onClick
event twice before triggering the onDoubleClick
when a pointing device is double-clicked and I needed to handle both events in different ways. I wrote a post back then implementing a solution (“Prevent click events on double click with React”) but it wasn’t until a few weeks ago that I found out that this solution can degrade the performance when managing these events.
Implementation using delay
Basically, we implemented a Hook that wraps both onClick
and onDoubleClick
handlers to delay every onClick
handler until we can assure that the click is not part of a double-click event (after 300ms without receiving a new onClick
event):
In the mentioned post you can also find the fully implemented solution besides another implementation using a HOC.
This solution apparently works well in most common use cases, you barely can notice the lag between the click and the result of that click on the browser.
The performance problem using delay
The problem using delay came when we use this solution in some more complex scenarios.
The setTimeout
function sets a timer which executes a function on the next event cycle once the timer expires. This means, the final delay will depend on the current status of the event loop. If the event loop is overloaded, the onClick
handler could have to wait more than 300ms to be executed, which leads to a stuttering user interface.
I came up against this problem while analyzing the performance of a complex app: A delayed onClick
handler took around 1200ms, 300ms of which were expended on the defined delay and 100ms on executing the callback. The onClick
handler was spending 800ms just waiting for the event loop to finish the enqueued work.
The way I found to “avoid” the JS event loop was by using the requestAnimationFrame method. The method allows you to tell the browser that you want it to perform a specified function before the next repaint.
Solution using requestAnimationFrame
A few weeks ago I wrote an entire post about this solution: “Implementing setTimeout using requestAnimationFrame”. You can have a look at it if you want to know more about how the solution works, but maybe you can extract the idea from the implementation itself:
Basically we implemented a “setTimeout
with higher priority” using requestAnimationFrame
under the hood. Instead of enqueuing the callback to the event loop when the delay time has passed like the setTimeout
does, we chain requestAnimationFrame
requests where every request checks if the delay time has passed and executes the callback if so, or enqueues a new request if not.
Implementation using requestTimeout
We use a custom Hook useClickPrevention
to wrap both callbacks onClick
and onDoubleClick
. To implement the boilerplate related with cancelling the scheduled current work we implemented another custom hook useCancelableScheduledWork
so we can extract the logic and use it from the useClickPrevention
Hook.
Maybe the trickiest thing in the solution is the way we implemented the support for cancelling the current pending request. We need to do it this way because the current request is changing with every new requestAnimationFrame
and we need the actual ref to the request to cancel it. A complete explanation is in the post we mentioned above “Implementing setTimeout using requestAnimationFrame”.
Summing up
If you want to handle both onClick
and onDoubleClick
, you will probably need to control the onClick
handler to not be triggered when the click
event comes from a dblClick
event. This post improves the solution proposed in the post “Prevent click events on double click with React” by using requestAnimationFrame
to work around the bottleneck due to the JS event loop.