I have a Next.js 14 project where I prioritize the use of server components over client components whenever possible.
The layout is server-rendered, while component wrappers used within it are client-rendered. One of these components is instantiated multiple times on the page, and part of its internal workings involves initializing an instance of a useBreakpoint
hook.
The purpose of the useBreakpoint
hook is to compute and return the current breakpoint state (a two-letter string) based on the screen size.
When we have multiple instances of this hook across the app, each instance computes the current breakpoint on every resize event. This results in a time complexity of O(n), where n is the number of components initializing the hook.
Since the main layout is server-rendered, singleton solutions like React Context or Redux wouldn’t work.
That’s why I came up with the following:
import { useEffect, useState } from 'react';
import { BreakpointKey, getBreakpointKey } from '@/lib/breakpoints';
const handleResize = () => {
const width = window.innerWidth;
const matchingBreakpoint = getBreakpointKey(width);
const event = new CustomEvent('breakpointChange', { detail: matchingBreakpoint });
window.dispatchEvent(event);
};
const useBreakpoint = () => {
const [breakpoint, setBreakpoint] = useState<BreakpointKey>('zr');
useEffect(() => {
const handleBreakpointChange = (event: CustomEvent<BreakpointKey>) => {
setBreakpoint(event.detail);
};
window.addEventListener('breakpointChange', handleBreakpointChange);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('breakpointChange', handleBreakpointChange);
};
}, []);
return breakpoint;
};
export default useBreakpoint;
So basically, we attach a single instance of the resize event handler whose only purpose is to fire a custom breakpointChange
event holding a payload of the computed breakpoint. Therefore, all instances of the hook can now register their own breakpointChange
event listeners, without each doing the same computation. So from O(n) for computing the breakpoint, this solution reduces it to O(1).
I haven’t encountered this approach mentioned anywhere in my brief research, so I wanted to share it here for your feedback. Are there any drawbacks I might have overlooked? For instance, is it worth creating a whole new event for this purpose? Thanks!
2
Answers
My Reply to Drew's answer,
I think you're missing some JavaScript and event listeners fundamentals here.
It doesn't actually instantiate both listeners for each call of the hook in a component. The
resize
event listener is instantiated and attached only once, contrary to your argument and this experiment proves it:The point is that it has to be an external dependency for this to work, if we define it inside of the hook per your statement then a new instance will be created every time, just like the 2nd part of the example from the screenshot.
Considering the two statements I put above are right, this argument falls out of context. We now know that the
resize
event listener is a form of singleton, i.e it's shared among the whole application since it's responsible for firing the custombreakpointChange
event.breakpointChange
event listener is instantiated for each instance of the hook but the heavy breakpoint computation is left inside of the singleresize
event listener, so I think that's still better than having multiple duplicatingresize
listeners computing the same thing for each component on the screen that uses the hook (per the suggested refactor given in Drew's answer).To continue my statement before the sidenote, cleaning up the
breakpointChange
event listener inside of theuseEffect
is needed since it's instantiated for each instance of the hook, which however is not the case for theresize
event listener, which is a singleton, a single instance that's re-used (shared) across the whole app to make a heavy computation once and distribute the result to all listeners attached to thebreakpointChange
event.It’s unclear what exactly you think you are optimizing here or how you get to
O(1)
constant time.From what I understand and see of the code you’ve still N components calling the
useBreakpoint
hook which instantiates both a"resize"
listener and a"breakpointChange"
event listener, so NuseBreakpoint
hook calls eventually trigger NhandleResize
andhandleBreakpointChange
calls. If anything this has created more work (though by a constant amount, soO(c * n)
orc * O(n)
by your reckoning) as compared to just doing the work in the"resize"
event handler. Additionally, theuseBreakpoint
hook neglects to cleanup the"resize"
listener.Suggested Refactor
"breakpointChange"
event dispatch and listener, all it does is map a"resize"
event to a"breakpointChange"
event.handleResize
handler into theuseEffect
hook callback body so it’s not an external dependency.