Reputation: 105
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
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:
windowDimension
is null
, and null <= 740
is true, so isMobile
is set to truediv>h1
element that you're expecting mobile visitors to seeuseEffect
hooks, the first of which calls a state setter passing a number greater than or equal to (presumably) 740, prompting a re-renderisMobile
is set to false
, updating the output to the full nav menuOnce 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
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