React Hook Optimised Event Listener

Geoffroy Mounier
4 min readNov 4, 2020

--

Though we find a lot of stuff on event listeners in react, here are some ways to handle things such as scroll or mouse events in an efficient and concise way.

Such events can fire a really high amount of times, therefore drastically impact performance of the overall rendering. Various strategies exist to reduce the cost and preserve the frame rates, which I will detail below.

In this story, the use case is with a scroll event, but could easily be replaced with numerous events : mouse events could be a good example since the amount of firing could be high there too.

For now, let’s write a simple hook that fires back when a passed ref is being scrolled:

Notice how we register the event in the useEffect , while not forgetting to unmount it by calling the removeEventListener in the return. So that when the user of the hooks unmount, we clean up.

We then simply call this hook wherever we need it:

const Component = () => {const containerRef = useRef(null);
const handleScroll => console.log('was scrolled');

useScrollListener(containerRef, handleScroll);
return (
<div ref={containerRef}>
{/* scrollable part */}
</div>
)
}

Now the problem is that the hook we wrote will fire as many times as a scroll gesture is detected. We certainly don’t need that many amount of firing … think of updating a boolean state in the Component for example : one firing would be enough !

Here are several ways of improving this hook.

Add a requestAnimationFrame

That would make sure the handler is doing its job only one time per frame rate.

It is an easy win, simple to implement, quite widely compatible across browsers, and that will surely improve the user experience by removing the lagging effects while scrolling.

Add a Timeout

A slightly more complex implementation, in the sense that you have more control in the amount of time the handler is called. Note that we can always keep the requestAnimationFrame so that even if the the timeout returns, the execution will wait until the next frame.

Now it is important to have in mind that even if the callback is controlled to be called only every throttle ms, the scroll event itself keeps firing at every scroll (thus at a high rate), thought it will not trigger anything particular between the throttles. While it is slightly better because we control for example re-render of the Component that uses this hook, it is still not ideal.

The main difference here is we introduced a timer, scrollingTimer that we store in a useRef so that we can access it easily across the hook. We only call the handleScroll when the timeout returns (after throttle ms). To avoid concurrent runs, we have to store the instance of the timeout in the scrollingTimer ref, and clear it at every scroll event. Of course we need to remove the listener when un-mounting. removeScrollListener is responsible for this (by both removing the event listener and clearing the timer).

Note that now calling the hooks becomes:

useScrollListener(containerRef, handleScroll, throttleTimeInMs); 

Unregister at every scroll event

This is the most elegant solution to me. I was very well advised by a colleague (cheers to him) and the logic is simple but does the job very nicely :

  • on hook mount: add the listener to scroll event, which calls the listenToScroll function when firing.
  • on listening: remove the scroll event, so that no further scrolling would call the listenToScroll until we decide to listen again.
  • still on listening: handle the logic (handleScroll).
  • still on listening: wrapped in a timeout, re-add the listener to the scroll event.

So for example, if the user keeps scrolling during throttle seconds, we do not call the handleScroll again. In fact, we do not call the listenToScroll at all!

This way, we make sure that the scroll event listen to the very first firing of scroll event and then snooze until we decide the register the event again. It can be every 1ms or every 1hour , depending on which throttle we pass.

Here again, we declare a scrollingTimer to safely clear the timeout when the user of the hooks unmount. I got rid of the removeScrollListener function and directly removed the listener, but feel free to implement it.

Other ways … IntersectionObserver

You can also control the scroll events of an HTML element by registering its children, so that they will fire the event when they become visible in the viewport.

More information here : https://developers.google.com/web/updates/2016/04/intersectionobserver?hl=en

That’s all ! Hope you’ll find some valuable snippets here ! Happy coding!

--

--