Search code examples
javascriptreactjsreact-hookswindow-resize

How can I access a state hook value from a callback passed to a listener?


I have a component that needs to listen to window resize, and hide/show sidebar appropriately. I decided to create a custom hook for this purpose, and it doesn't seem to work properly.

The main problem is that the mainSidebar value is always constant, and the update is not being read properly.

This is the hook itself:

export function useResizeListener(cb: (this: Window, e: UIEvent) => any) {
    const [size, setSize] = React.useState({ width: window.outerWidth, height: window.outerHeight })

    React.useEffect(() => {
        function _wrapped(this: Window, e: UIEvent): any {
            console.debug('setting size and calling callback', { width: this.outerWidth, height: this.outerHeight })
            setSize({ width: this.outerWidth, height: this.outerHeight })
            cb.call(this, e)
        }
        window.addEventListener('resize', _wrapped)
        return () => window.removeEventListener('resize', _wrapped)
    }, [size.width, size.height])
}

And this is the component using it:

const shouldHaveSidebar = () => document.body.clientWidth >= ScreenSizes.Tablet

const HomePage: React.FunctionComponent<IProps> = (props) => {
    const [mainSidebar, toggleMainSidebar] = React.useState(shouldHaveSidebar())

    useResizeListener(() => {
        console.debug('device width vs tablet width:', document.body.clientWidth, '>=', ScreenSizes.Tablet)
        console.debug('shouldHaveSidebar vs mainSidebar', shouldHaveSidebar(), '!==', mainSidebar)
        if (shouldHaveSidebar() !== mainSidebar) {
            console.debug('setting to', shouldHaveSidebar())
            toggleMainSidebar(shouldHaveSidebar())
        }
    })
    return ...
}

Upon toggling viewport size on the Chrome dev inspector device modes, I get this output in the console. Note I started with desktop and then changed to mobile L.

setting size and calling callback {width: 1680, height: 1027}
device width vs tablet width: 1146 >= 800
shouldHaveSidebar vs mainSidebar true !== false
setting to true

setting size and calling callback {width: 425, height: 779}
device width vs tablet width: 425 >= 800
shouldHaveSidebar vs mainSidebar false !== false // <--- should be: false !== true
  1. I tried moving the _wrapper function between outside and inside the useEffect hook
  2. I tried wrapping the callback in a setTimeout/setImmediate
  3. I tried wrapping the state get (mainSidebar bool) with a function to retrieve it which is defined in the component and outside the hook call

Neither of these worked.

What am I doing wrong?


Solution

  • I would suggest restructuring the solution. First I would change the useResizeListener into a hook that gives you the window size:

    export function useWindowSize() {
      const [size, setSize] = React.useState({ width: window.clientWidth, height: window.clientHeight });
    
      React.useEffect(() => {
        function handleResize(e: UIEvent) {
          setSize({ width: window.clientWidth, height: window.clientHeight });
        }
    
        window.addEventListener('resize', handleResize);
    
        return () => window.removeEventListener('resize', handleResize);
      }, []);
    
      return size;
    }
    

    Then in your HomePage component:

    const shouldHaveSidebar = (size) => size.width >= ScreenSizes.Tablet
    
    const HomePage: React.FunctionComponent<IProps> = (props) => {
      const size = useWindowSize();
      const mainSidebar = shouldHaveSidebar(size);
    
      // Now if you want to perform an action when the size changes you cna simply do:
      useEffect(() => {
        // Perform your action here
      }, [size.width, size.height]);
    
      // ...
    }