StephD
StephD

Reputation: 304

gatsby + react hook masonry breaks on build because there is no window

I have a react hook set up to handle masonry on certain pages of my gatsby site. The problem is it references the window object, which does not exist on the server side gatsby build. I've read that the solution is to wrap useEffect with:

if (typeof window === 'undefined') {
}

however I just can't seem to wrap the right part of my masonry file. I've also read that using the above hack makes the server side rendering sort of pointless, not sure.

Could someone tell me where that if statement should go in my masonry file below? It's not a plugin, it's a hook in my utils folder. Using code from this tut. I've tried the if statement inside the useEffects, around the useEffects, around the whole eventListener, but no dice. Thank you!!

import React, { useEffect, useRef, useState } from "react"
import styled from "styled-components"

const useEventListener = (eventName, handler, element = window) => {
  const savedHandler = useRef()

  useEffect(() => {
    savedHandler.current = handler
  }, [handler])

  useEffect(() => {
    const isSupported = element && element.addEventListener
    if (!isSupported) return

    const eventListener = event => savedHandler.current(event)
    element.addEventListener(eventName, eventListener)

    return () => {
      element.removeEventListener(eventName, eventListener)
    }
  }, [eventName, element])
}

const fillCols = (children, cols) => {
  children.forEach((child, i) => cols[i % cols.length].push(child))
}

export default function Masonry({ children, gap, minWidth = 500, ...rest }) {
  const ref = useRef()
  const [numCols, setNumCols] = useState(3)
  const cols = [...Array(numCols)].map(() => [])
  fillCols(children, cols)

  const resizeHandler = () =>
    setNumCols(Math.ceil(ref.current.offsetWidth / minWidth))
  useEffect(resizeHandler, [])
  useEventListener(`resize`, resizeHandler)

  const MasonryDiv = styled.div`
    margin: 1rem auto;
    display: grid;
    grid-auto-flow: column;
    grid-gap: 1rem;
  `
  const Col = styled.div`
    display: grid;
    grid-gap: 1rem;
  `

  return (
    <MasonryDiv ref={ref} gap={gap} {...rest}>
      {[...Array(numCols)].map((_, index) => (
        <Col key={index} gap={gap}>
          {cols[index]}
        </Col>
      ))}
    </MasonryDiv>
  )
}

Upvotes: 1

Views: 622

Answers (1)

Ferran Buireu
Ferran Buireu

Reputation: 29320

In your gatsby-node.js add the following snippet:

exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
  if (stage === "build-html") {
    actions.setWebpackConfig({
      module: {
        rules: [
          {
            test: /masonry/,
            use: loaders.null(),
          },
        ],
      },
    })
  }
}

Note: use /masonry/ library path from your node_modules.

From Gatsby documentation about debugging HTML builds:

Errors while building static HTML files generally happen for one of the following reasons:

Some of your code references “browser globals” like window or document. If this is your problem you should see an error above like “window is not defined”. To fix this, find the offending code and either a) check before calling the code if window is defined so the code doesn’t run while Gatsby is building (see code sample below) or b) if the code is in the render function of a React.js component, move that code into a componentDidMount lifecycle or into a useEffect hook, which ensures the code doesn’t run unless it’s in the browser.

Alternatively, you can wrap your import your Masonry hook usage inside this statement:

if (typeof window !== `undefined`) {
  const module = require("module")
}

Note the !== comparison, not the === like the one you've provided.

Upvotes: 2

Related Questions