You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 18:54:03