I was recently tasked with fixing some UI bugs at work. Our navbar wasn’t refreshing with more items when the browser was resized. I thought to myself “This should be a breeze to fix”.
I thought it was obvious which methodology to use – this led me to “the obvious but wrong” solution of using the resize
event.
My first step was to add an event listener for the resize
event, which fires every time the document view is resized. This event listener triggered an event handling callback that I defined. I then used the useEffect
hook to create and remove event listeners for the component I was working on. The resize
event listener is added on page mount, and cleaned up on unmount with a return statement of the useEffect
hook. So far so good. Next, I wrapped the event handler in a useCallback
to ensure that resizeHandler
does not change between re-renders (i.e. it gets memoized). Thus I was able to safely use it as a dependency of the useEffect
hook.
The code matching the above specification is shown below:
import { useCallback, useEffect } from 'react';
const resizeHandler = useCallback(() => {
// does callback stuff
}, []);
useEffect(() => {
window.addEventListener('resize', resizeHandler);
return () => {
window.removeEventListener('resize', resizeHandler);
};
}, [resizeHandler]);
The above code did work, but at a cost of slowing down the UI. Not a great start… At this point, I wasn’t sure what went wrong.
To understand the problem, I modified the above code to measure the number of times resizeHandler
is called. I declared a state variable called counter
and modified resizeHandler
to increment the value of counter
by 1 every time the callback is called (see the code snippet below).
import { useState } from 'react';
const [counter, setCounter] = useState(0);
const resizeHandler = useCallback(() => {
// do callback stuff, AND then...
setCounter((c) => c + 1);
}, []);
//
return <>{counter}</>;
Resizing the browser from full to half width on my 16inch monitor triggered resizeHandler
30 times. Whoa! If more of such resizeHandlers
were included in the code, it would force too many re-renders, making the page unbearably slow… This is certainly not a production worthy fix.
My problem has now become much more specific: how can I handle the resize
event, without overwhelming the app with too many re-renders? The answer is: debouncing Debouncing limits the rate at which the function fires. My favourite tool for the job is a library called use-debounce
(link). Creating a debounced callback was as easy as replacing useCallback
with useDebouncedCallback
imported from the library. One additional parameter accepted by useDebouncedCallback
is the time delay in milliseconds. After a few minutes of tinkering, I realised that 200ms should do the trick.
import { useDebouncedCallback } from 'use-debounce/lib';
const handleResize = useDebouncedCallback(
() => setSlowCounter((c) => c + 1),
200
);
The effects of this change were significant - the new callback fired only once where the old one fired 30 times (note that your numbers may vary, depending on how quickly or slowly you resize the browser).
“The non-obvious but right” solution is what ultimately got pushed into production!