Preventing click events on double click with React, the performant way

Ceci García García
Trabe
Published in
3 min readMar 30, 2020
Photo by Ciprian Boiciuc on Unsplash

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.

--

--