我试图在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
Gatsby进行服务器端渲染,包括在Node环境中渲染React组件,并将生成的标记保存为静态文件。当有人在生产中访问您的某个页面时(或者如果您在开发中使用SSR,则在开发中(,React会呈现您的组件,并将它们与已经可见的DOM节点关联,这一过程被称为"再水合"。
这一切都很重要,因为useEffect
(或基于类的API方法,如componentDidMount
(在SSR期间不运行。它们只在客户端重新水合代码后运行。此外,如果服务器端生成的DOM节点与React在初始渲染时(在任何useEffect
挂钩运行之前(在客户端渲染的内容不匹配,则最终会出现水合不匹配错误,提示React丢弃现有的DOM节点,并用它在客户端生成的内容替换它们。
有了这些信息,你就可以开始调试可能导致意外内容闪光的原因以及如何解决:
- 服务器端,
windowDimension
为null
,null <= 740
为true,因此isMobile
设置为true - 服务器端生成的输出显示您希望移动访问者看到的
div>h1
元素 - 客户端,React重新水合并激发
useEffect
钩子,第一个钩子调用一个传递大于或等于(大概(740的数字的状态设置器,提示重新渲染 - 组件使用更新后的值重新渲染,
isMobile
设置为false
,将输出更新为完整导航菜单
Once解决此问题的方法是等待渲染标记或渲染占位符,直到您在浏览器中进行渲染:
// 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更新/布局/绘制周期:使用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树很大,额外的标记也会减慢速度。和任何事情一样,为您自己的用例做一些测试,并选择最适合您和您的团队的用例。
您的页面以windowDimension
呈现为null
,因此当windowDimension
更宽的用户访问您的页面时,他们会在React启动之前看到移动布局的短暂闪光&渲染正确的。
您可以使用@media查询来解决此问题。