Next.js编译运行后React组件HTML元素错位合并问题排查
问题原因
你遇到的是典型的SSR/SSG hydration 不匹配问题:react-responsive的useMediaQuery钩子在服务端渲染阶段无法获取客户端的真实视口尺寸,默认会返回false,因此服务端输出的HTML是移动端导航栏的结构。
客户端hydrate阶段,React会默认复用服务端生成的DOM节点,此时客户端已经能拿到视口尺寸,isLarge返回true应该渲染侧边栏,但React发现现有DOM结构和预期渲染的结构不匹配,就会出现DOM节点错误复用、结构合并错乱的问题。你调整窗口大小后组件重新渲染,此时已经跳过了hydrate阶段,所以结构就恢复正常了。
解决方法
你可以任选以下任意一种方案修复:
方案1:添加挂载状态保证hydrate阶段结构一致
新增一个挂载状态,组件未挂载时服务端和客户端输出完全一致的结构,挂载后再根据真实的视口尺寸渲染对应组件:
import React, { useState, useEffect } from "react"; import { useMediaQuery } from "react-responsive"; import { BellIcon, ChatAlt2Icon, CogIcon, MenuIcon, } from "@heroicons/react/outline"; export const Sidebar: React.FC = ({ children }) => { const [isOpen, setOpen] = useState(false); const [mounted, setMounted] = useState(false); const isLarge = useMediaQuery({ query: "(min-width: 768px)" }); // 组件挂载后再渲染对应导航 useEffect(() => { setMounted(true); }, []); // 未挂载时可以返回加载占位,也可以返回一个空白的通用结构 if (!mounted) return <div className="flex flex-row w-screen h-screen"></div>; return ( <div className={"flex flex-row w-screen h-screen"}> {/* Sidebar starts */} {(isOpen || isLarge) && ( <div id="sidebar" className={"relative w-64 bg-gray-800 h-screen"} > <div className="flex flex-col justify-between"> <div className="px-8"> <ul className="mt-12"></ul> </div> <div className="px-8 border-t border-gray-700" id="bottom-bar" > <ul className="w-full flex items-center justify-between bg-gray-800"> <li className="cursor-pointer text-white pt-5 pb-3"> <BellIcon className="icon icon-tabler icon-tabler-bell" width={20} height={20} viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" ></BellIcon> </li> <li className="cursor-pointer text-white pt-5 pb-3"> <ChatAlt2Icon className="icon icon-tabler icon-tabler-bell" width={20} height={20} viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" ></ChatAlt2Icon> </li> <li className="cursor-pointer text-white pt-5 pb-3"> <CogIcon className="icon icon-tabler icon-tabler-bell" width={20} height={20} viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" ></CogIcon> </li> </ul> </div> </div> </div> )} {/* Sidebar ends */} <div className="flex flex-col w-full h-full"> {/* mobile navbar starts */} {!isLarge && ( <div id="mobile-nav" className="flex h-16 w-full relative bg-gray-800 flex-row items-center p-3" > <div className="cursor-pointer text-white pt-5 pb-3"> <MenuIcon className={"icon icon-tabler icon-tabler-bell"} width={30} height={30} stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" onClick={() => { setOpen(!isOpen); console.log(isOpen); }} ></MenuIcon> </div> </div> )} {/* mobile navbar ends */} <div className="flex w-full h-full rounded relative"> {children} </div> </div> </div> ); };
方案2:给条件渲染块添加唯一key,强制React不复用节点
给两个条件渲染的根元素分别添加唯一的key属性,让React识别到这是完全不同的节点,不会错误复用:
{(isOpen || isLarge) && ( <div key="desktop-sidebar" id="sidebar" className={"relative w-64 bg-gray-800 h-screen"} > {/* 侧边栏内容 */} )} {!isLarge && ( <div key="mobile-navbar" id="mobile-nav" className="flex h-16 w-full relative bg-gray-800 flex-row items-center p-3" > {/* 移动端导航内容 */} )}
方案3:配置useMediaQuery的初始值适配SSR
如果你的项目是SSR,可以直接给useMediaQuery配置初始值,让服务端渲染的结构和客户端一致:
// 服务端可以根据User-Agent判断设备类型传入对应的初始值,客户端会自动接管后续的响应式判断 const isLarge = useMediaQuery( { query: "(min-width: 768px)" }, undefined, { initialValue: true } // 桌面端优先的场景可以直接设为true );
内容的提问来源于stack exchange,提问作者Nikola-Milovic
相关产品推荐
相关产品推荐

