React/Gatsby: 错误的 div 在条件渲染时短暂加载

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

我正在尝试在 Gatsby 中有条件地呈现 div 以构建响应式导航菜单。不幸的是,在加载完整的导航菜单之前,我快速浏览了菜单 div。任何解决此问题的提示或技巧将不胜感激!

    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

您的页面使用 windowDimension 呈现为 null,因此当 windowDimension 更宽的人访问您的页面时,他们会在 React 启动之前看到移动布局的短暂闪现输入并渲染正确的。

您可以改用@media 查询来解决这个问题。

Gatsby 进行服务器端渲染,这涉及在 Node 环境中渲染 React 组件并将生成的标记保存为静态文件。当有人在生产中访问您的某个页面时(如果您在开发中使用 SSR,则在开发中),React 会呈现您的组件并将它们与已经可见的 DOM 节点相关联,这一过程称为“再水化” .

了解这一点很重要,因为 useEffect(或基于 class 的 API 方法,如 componentDidMount)在 SSR 期间不会 运行。他们只 运行 一旦代码在客户端重新水化。此外,如果服务器端生成的 DOM 节点与 React 在初始渲染时在客户端渲染的节点不匹配(在任何 useEffect 钩子 运行 之前),您最终会得到水合作用不匹配错误提示 React 丢弃存在的 DOM 节点并用它在客户端生成的节点替换它们。

有了这些信息,您就可以开始调试可能会导致突然出现意外内容的情况以及如何解决它:

  1. 服务器端,windowDimensionnull,而null <= 740为真,所以isMobile设置为真
  2. 然后服务器端生成的输出会显示您希望移动访问者看到的 div>h1 元素
  3. 客户端,React 再水合并触发 useEffect 钩子,第一个钩子调用状态 setter 传递大于或等于(大概)740 的数字,提示重新渲染
  4. 组件使用更新后的值重新呈现,isMobile 设置为 false,将输出更新为完整的导航菜单

解决此问题的一种方法是等待呈现标记或呈现占位符,直到您在浏览器中呈现:

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

如上所述,问题在于您最终会在服务器端生成与浏览器中不同的标记,导致 React 丢弃 DOM 节点并替换它们。相反,您可以像这样利用 useEffect 来确保初始渲染与服务器端输出匹配:

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>
}

还有另一种方法适用于许多场景,可以避免额外的渲染,DOM update/layout/paint 循环:使用 CSS 隐藏其中一个 DOM 分支相关:

/** @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>
)

我尽可能喜欢这种方法,因为它减少了加载时的布局抖动,但如果 DOM 树很大,额外的标记也会减慢速度。与任何事情一样,对您自己的用例进行一些测试,select 什么最适合您和您的团队。