React Hook Optimised Event Listener
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!