Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gatsby: React conditional rendering based on window.innerWidth misbehaving

Conditional rendering of components based on window.innerWidth seems to not work as intended just in the production build of Gatsby based website.

The hook I am using to check the viewport's width, with the additional check for the window global to avoid Gatsby-node production build errors, is the following:

import { useState, useEffect } from 'react'

const useWindowWidth = () => {
  const windowGlobal = typeof window !== 'undefined'

  if(windowGlobal) {
    const [width, setWidth] = useState(window.innerWidth)

    useEffect(() => {
      const handleResize = () => setWidth(window.innerWidth)
      window.addEventListener('resize', handleResize)
      return () => {
        window.removeEventListener('resize', handleResize)
      }
    })

    return width
  }
}

export default useWindowWidth

Then in my actual component I do the following:

IndexPage.Booking = () => {
  const windowWidth = useWindowWidth()

  return (
    <div className="section__booking__wrapper">
      { windowWidth <= mediaQueries.lg && <IndexPage.Cta /> }
      <div className="section__booking-bg" style={{ backgroundImage: `url(${bg})` }}>
        { windowWidth > mediaQueries.lg && <IndexPage.Cta /> }
      </div>
    </div>
  )
}

It works as it should in development but the production build fails to render:

<div className="section__booking-bg" style={{ backgroundImage: `url(${bg})` }}>

When resizing the window below the mediaQueries.lg (1024) it then triggers the actual normal behaviour or conditionally rendering mobile and desktop versions of the component.

To doublecheck if it was because the render triggers on just the resize event (which it doesn't as it works on load in development environment) I also simply, from within the hook console.log() the return value and it gets printed, in production correctly on load.

There are also no errors or warnings in the production or development build whatsoever.

Edit as per @Phillip 's suggestion

const useWindowWidth = () => {
  const isBrowser = typeof window !== 'undefined'
  const [width, setWidth] = useState(isBrowser ? window.innerWidth : 0)

  useEffect(() => {
    if (!isBrowser) return false

    const handleResize = () => setWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)

    return () => {
      window.removeEventListener('resize', handleResize)
    }
  })

  return width
}

It now works just when you resize it, once, under the mediaQueries.lg threshold and then it works flawlessly across desktop and mobile but not on load.

like image 229
Mel Macaluso Avatar asked Oct 29 '19 13:10

Mel Macaluso


2 Answers

I had a similar problem to this and haven't found a solution, but a work around. Put the following at the start of your render:

if (typeof window === `undefined`) {
    return(<></>);
}

What I think is happening is that Gatsby is building the page with a style based off the window width (which will be 0 / undefined). Then it's not updating the style in the DOM once the page loads as it thinks it has already performed that action. I think this is a small bug in Gatsby maybe?

Either way, the above renders your component blank during the build, forcing it to fully respect all logic when the page loads. Hopefully that provides a solution albeit not a satisfying/complete explanation :)

like image 153
Jamie Robinson Avatar answered Oct 21 '22 08:10

Jamie Robinson


I'm guessing it is too late to answer but calling handleResize before adding the event listener should work. Here is a code I used for same purpose:

  useEffect(() => {
  setWidth(window.innerWidth);
  window.addEventListener("resize", () => {
    setWidth(window.innerWidth);
  });
  return () => {
    window.removeEventListener("resize", () => {});
  };
}, []);
like image 2
Sterlin V Avatar answered Oct 21 '22 08:10

Sterlin V