skip to Main Content

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


  1. Chosen as BEST ANSWER

    My Reply to Drew's answer,

    I think you're missing some JavaScript and event listeners fundamentals here.

    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

    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: event listeners experiment

    Move the handleResize handler into the useEffect hook callback body so it's not an external dependency.

    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.

    Additionally, the useBreakpoint hook neglects to cleanup the "resize" listener.

    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 custom breakpointChange event.

    • Sidenote: A breakpointChange event listener is instantiated for each instance of the hook but the heavy breakpoint computation is left inside of the single resize event listener, so I think that's still better than having multiple duplicating resize 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 the useEffect is needed since it's instantiated for each instance of the hook, which however is not the case for the resize 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 the breakpointChange event.


  2. 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 N useBreakpoint hook calls eventually trigger N handleResize and handleBreakpointChange calls. If anything this has created more work (though by a constant amount, so O(c * n) or c * O(n) by your reckoning) as compared to just doing the work in the "resize" event handler. Additionally, the useBreakpoint hook neglects to cleanup the "resize" listener.

    Suggested Refactor

    • Remove the "breakpointChange" event dispatch and listener, all it does is map a "resize" event to a "breakpointChange" event.
    • Properly maintain event listeners, including removing them when components unmount.
    • Move the handleResize handler into the useEffect hook callback body so it’s not an external dependency.
    import { useEffect, useState } from 'react';
    import { BreakpointKey, getBreakpointKey } from '@/lib/breakpoints';
    
    const useBreakpoint = () => {
      const [breakpoint, setBreakpoint] = useState<BreakpointKey>('zr');
    
      useEffect(() => {
        const handleResize = () => {
          const width = window.innerWidth;
          const matchingBreakpoint = getBreakpointKey(width);
          setBreakpoint(matchingBreakpoint);
          // or setBreakpoint(getBreakpointKey(window.innerWidth));
        };
    
        window.addEventListener('resize', handleResize);
    
        // Invoke once to set state for initial window width
        handleResize();
        
        return () => {
          window.removeEventListener('resize', handleResize);
        };
      }, []);
    
      return breakpoint;
    };
    
    export default useBreakpoint;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search