how to conditionally render a mobile nav in gatsby?

1.1k Views Asked by At

I am new to gatsby and server side rendering, although I've worked with react quite a bit before. I wrote a bit of code that checks for the window size and then renders either the nav or the mobile nav based on the width. However, when I tried to test-deploy, I discovered that the window object is not available when you use server side rendering. My question is, is there any way i can modify my code to still work? or should I just rebuild everything with sass and media queries?

import React, { useState, useEffect } from "react"
import { Link, graphql, useStaticQuery } from "gatsby"
// import "./header.module.scss"
import HeaderStyles from "./header.module.scss"
import Logo from "../images/[email protected]"
import Nav from "./nav"
const Header = () => {
  const data = useStaticQuery(graphql`
    query MyQuery {
      site {
        siteMetadata {
          title
        }
      }
    }
  `)
  const [isDesktop, setDesktop] = useState(window.innerWidth > 768)

  const updateMedia = () => {
    setDesktop(window.innerWidth > 768)
  }

  useEffect(() => {
    window.addEventListener("resize", updateMedia)
    return () => window.removeEventListener("resize", updateMedia)
  })
  const [menuActive, setMenuState] = useState(false)
  return (
    <header
      className={menuActive ? HeaderStyles.mobileNav : HeaderStyles.header}
    >
      <Link to="/" className={HeaderStyles.title}>
        {/* {data.site.siteMetadata.title} */}
        <img src={Logo} height="60" />
      </Link>
      {isDesktop ? (
        <Nav />
      ) : (
        //hamburger
        <div
          onClick={() => setMenuState(!menuActive)}
          className={`${HeaderStyles.navIcon3} ${
            menuActive ? HeaderStyles.open : ""
          }`}
        >
          <span></span>
          <span></span>
          <span></span>
          <span></span>
        </div>
      )}
      {menuActive ? <Nav /> : null}
    </header>
  )
}

export default Header

2

There are 2 best solutions below

0
On

You can't use the window object (or other global objects such as document) directly in your code. This is because gatsby develop is rendered by the browser, where there's a window object, however gatsby build occurs in the Node server (your machine or you build system) where's there's no window.

If you don't want to redo your code with SCSS and mediaqueries (preferred version and more native), you need to make a condition above every use of window object. In your case, you need to make it when the DOM tree is loaded (useEffect with empty deps, []). Something like this should work:

import React, { useState, useEffect } from "react"
import { Link, graphql, useStaticQuery } from "gatsby"
// import "./header.module.scss"
import HeaderStyles from "./header.module.scss"
import Logo from "../images/[email protected]"
import Nav from "./nav"
const Header = () => {
  const data = useStaticQuery(graphql`
    query MyQuery {
      site {
        siteMetadata {
          title
        }
      }
    }
  `)
  const [isDesktop, setDesktop] = useState(false)

  useEffect(()=>{
    if(typeof window !== 'undefined'){
      setDesktop(window.innerWidth > 768)
    }
  },[])

  const updateMedia = () => {
    setDesktop(window.innerWidth > 768)
  }

  useEffect(() => {
    window.addEventListener("resize", updateMedia)
    return () => window.removeEventListener("resize", updateMedia)
  })
  const [menuActive, setMenuState] = useState(false)
  return (
    <header
      className={menuActive ? HeaderStyles.mobileNav : HeaderStyles.header}
    >
      <Link to="/" className={HeaderStyles.title}>
        {/* {data.site.siteMetadata.title} */}
        <img src={Logo} height="60" />
      </Link>
      {isDesktop ? (
        <Nav />
      ) : (
        //hamburger
        <div
          onClick={() => setMenuState(!menuActive)}
          className={`${HeaderStyles.navIcon3} ${
            menuActive ? HeaderStyles.open : ""
          }`}
        >
          <span></span>
          <span></span>
          <span></span>
          <span></span>
        </div>
      )}
      {menuActive ? <Nav /> : null}
    </header>
  )
}

export default Header

Keep in mind that this approach will create a small blink (a few ms) until the code knows what's the window's with and, depending on your use-case, it may not work when resizing it, we should readapt a little bit the code to make it functional there.

0
On

For now, you can use the gatsby-plugin-breakpoints https://www.gatsbyjs.com/plugins/gatsby-plugin-breakpoints/?=break

import { useBreakpoint } from 'gatsby-plugin-breakpoints';

import MobileOnlyComponent from './your/component/path';
// ...

const MyComponent = () => {
    const breakpoints = useBreakpoint();

    return (
        <AnyComponent>
            {/* Anything */}

            {/* <MobileOnlyComponent /> will only be displayed if max-width <= 320px  */}
            {breakpoints.xs ? <MobileOnlyComponent /> : null}
        </AnyComponent>
    );
};

export default MyComponent;