thomtheetoad
thomtheetoad

Reputation: 105

React/Gatsby: Wrong div loads for a brief moment when conditional rendering

I'm trying to conditionally render a div in Gatsby in an effort to build a responsive nav menu. Unfortunately, I'm getting a quick flash of the menu div just before the full navigation menu loads. Any tips or tricks to resolve this would be appreciated!

    import React, { useEffect, useState } from "react"
    import * as navlinksStyles from "./navlinks.module.scss"
    
    const NavLinks = () => {
      const [windowDimension, setWindowDimension] = useState(null)
    
      useEffect(() => {
        setWindowDimension(window.innerWidth)
      }, [])
    
      useEffect(() => {
        function handleResize() {
          setWindowDimension(window.innerWidth)
        }
    
        window.addEventListener("resize", handleResize)
        return () => window.removeEventListener("resize", handleResize)
      }, [])
    
      const isMobile = windowDimension <= 740
    
      return (
        <div className={navlinksStyles.wrapper}>
          {isMobile ? (
            <div>
              <h1 className={navlinksStyles.menu}>menu</h1>
            </div>
          ) : (
            <div className={navlinksStyles.navlinksWrapper}>
              <ul>
                <li>About</li>
                <li>Services</li>
                <li>Frames</li>
                <li>Lenses</li>
                <li>Locations</li>
                <li>Mailbox</li>
              </ul>
            </div>
          )}
        </div>
      )
    }
    
    export default NavLinks

Upvotes: 1

Views: 757

Answers (2)

coreyward
coreyward

Reputation: 80090

Gatsby does server-side rendering, which involves rendering your React components in a Node environment and saving out the markup produced as a static file. When someone visits one of your pages in production (or in development if you're using SSR in dev), React renders your components and associates them with the already-visible DOM nodes in a process referred to as “rehydration”.

This is all important to know because useEffect (or class-based API methods like componentDidMount) don't run during SSR. They only run once the code is rehydrated client-side. Further, if the DOM nodes produced server-side don't match up with what React renders client-side on the initial render (before any useEffect hooks run), you wind up with a hydration mismatch error that prompts React to throw away the DOM nodes that exist and replace them with what it has produced client-side.

Armed with this info, you can start to debug what might be happening to cause a flash of unexpected content and how to address it:

  1. Server-side, windowDimension is null, and null <= 740 is true, so isMobile is set to true
  2. The output produced server-side then shows the div>h1 element that you're expecting mobile visitors to see
  3. Client side, React rehydrates and fires useEffect hooks, the first of which calls a state setter passing a number greater than or equal to (presumably) 740, prompting a re-render
  4. The component re-renders with the updated value and isMobile is set to false, updating the output to the full nav menu

Once approach to solving this is to wait to render markup or render a placeholder until you’re rendering in a browser:

// Note: do NOT do this!
const NavLinks = () => {
  if (typeof window === "undefined") return null
  
  return (
    <div>Your content</div>
  )
}

The problem with this, as alluded to above, is that you wind up producing different markup server-side than you do in a browser, causing React to throw away DOM nodes and replace them. Instead, you can ensure the initial render matches the server-side output by leveraging useEffect like so:

const NavLinks = () => {
  const [ready, setReady] = useState(null)
  useEffect(() => { setReady(true) }, [])
  
  // note: the return value of an `&&` expression is the value of the first
  // falsey condition, or the last condition if all are truthy, so if `ready`
  // has not been updated, this evaluates to `return null`, and otherwise, to
  // return <div>Your content</div>.
  return ready && <div>Your content</div>
}

There is another approach that works in many scenarios that will avoid the extra render, DOM update/layout/paint cycle: use CSS to hide one of the DOM branches as relevant:

/** @jsx jsx */
import { jsx } from "@emotion/core"

const mobile = "@media (max-width: 740px)"

const NavLinks = () => (
  <div>
    <div css={{ display: "none", [mobile]: { display: "block" } }}>
      Mobile menu
    </div>
    <div css={{ [mobile]: { display: "none" } }}>
      Desktop menu
    </div>
  </div>
)

I prefer this approach when possible as it cuts down on layout thrashing on-load, but if the DOM trees are large, the extra markup can slow things down as well. As with anything, do some testing for your own use cases and select what works best for you and your team.

Upvotes: 3

Derek Nguyen
Derek Nguyen

Reputation: 11577

Your page is rendered with windowDimension as null, so when someone with a wider windowDimension visit your page, they'll see a brief flash of mobile layout before React kicks in & render the correct one.

You can get around this by using @media query instead.

Upvotes: 2

Related Questions