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

NextJS Hydration失败报错求助:导航栏功能正常但水合不匹配

修复NextJS中多级导航栏的Hydration失败错误

出现Hydration failed错误的核心原因是服务器端渲染(SSR)的UI和客户端首次渲染的UI不一致,你的代码里有两处关键问题:

  1. 直接在渲染阶段访问window.innerWidth——服务器环境没有window对象,会导致服务器和客户端渲染出不同的DOM结构
  2. 用typeof window !== 'undefined'做条件渲染,服务器端会跳过按钮渲染,客户端却会渲染,直接引发Hydration不匹配

修复步骤

1. 用状态管理窗口宽度

不要直接在渲染时读window.innerWidth,改用useState存窗口宽度,配合useEffect在客户端初始化并监听窗口 resize:

const [windowWidth, setWindowWidth] = useState(0);

useEffect(() => {
  // 客户端初始化宽度
  setWindowWidth(window.innerWidth);
  // 监听窗口大小变化
  const handleResize = () => setWindowWidth(window.innerWidth);
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

2. 统一服务器与客户端的初始渲染逻辑

去掉(typeof window !== 'undefined') && <button>这种条件,服务器端无法识别window,会导致DOM结构差异。改为始终渲染按钮,用状态控制显示逻辑。

3. 优化下拉菜单的状态触发逻辑

把依赖window.innerWidth的判断换成状态windowWidth,避免在渲染阶段直接访问浏览器API。


修改后的完整代码

import { useState, useEffect, useRef } from 'react';
import Dropdown from './Dropdown';
import Link from 'next/link';

const MenuItems = ({ items, depthLevel }) => {
  const [dropdown, setDropdown] = useState(false);
  const [windowWidth, setWindowWidth] = useState(0);
  const ref = useRef();

  useEffect(() => {
    // 初始化窗口宽度
    const initWidth = window.innerWidth;
    setWindowWidth(initWidth);
    
    // 监听窗口 resize
    const handleResize = () => setWindowWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);

    // 点击外部关闭下拉
    const handleClickOutside = (event) => {
      if (dropdown && ref.current && !ref.current.contains(event.target)) {
        setDropdown(false);
      }
    };
    document.addEventListener('mousedown', handleClickOutside);
    document.addEventListener('touchstart', handleClickOutside);

    return () => {
      window.removeEventListener('resize', handleResize);
      document.removeEventListener('mousedown', handleClickOutside);
      document.removeEventListener('touchstart', handleClickOutside);
    };
  }, [dropdown]);

  const onMouseEnter = () => {
    windowWidth > 960 && setDropdown(true);
  };

  const onMouseLeave = () => {
    windowWidth > 960 && setDropdown(false);
  };

  const closeDropdown = () => {
    dropdown && setDropdown(false);
  };

  return (
    <li
      className="menu-items"
      ref={ref}
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
      onClick={closeDropdown}
    >
      {items.url && items.submenu ? (
        <>
          <button
            type="button"
            aria-haspopup="menu"
            aria-expanded={dropdown ? 'true' : 'false'}
            onClick={() => setDropdown((prev) => !prev)}
          >
            {windowWidth < 960 && depthLevel === 0 ? (
              items.title
            ) : (
              <Link href={items.url}>{items.title}</Link>
            )}

            {depthLevel > 0 && windowWidth < 960 ? null : 
              depthLevel > 0 && windowWidth > 960 ? (
                <span>&raquo;</span>
              ) : (
                <span className="arrow" />
              )
            }
          </button>
          <Dropdown
            depthLevel={depthLevel}
            submenus={items.submenu}
            dropdown={dropdown}
          />
        </>
      ) : !items.url && items.submenu ? (
        <>
          <button
            type="button"
            aria-haspopup="menu"
            aria-expanded={dropdown ? 'true' : 'false'}
            onClick={() => setDropdown((prev) => !prev)}
          >
            {items.title}{' '}
            {depthLevel > 0 ? (
              <span>&raquo;</span>
            ) : (
              <span className="arrow" />
            )}
          </button>
          <Dropdown
            depthLevel={depthLevel}
            submenus={items.submenu}
            dropdown={dropdown}
          />
        </>
      ) : (
        <Link href={items.url}>{items.title}</Link>
      )}
    </li>
  );
};

export default MenuItems;

额外说明

  • 所有依赖浏览器环境的API(如window、document)都要放在useEffect中执行,确保只在客户端运行
  • 服务器端渲染时,React会先生成静态HTML,客户端挂载时会对比这个HTML,如果结构不一致就会抛出Hydration错误,所以要确保前后端渲染的DOM结构完全一致
  • 如果某些内容只能在客户端渲染,可以用useEffect延迟渲染,或者用NextJS的dynamic组件关闭SSR

内容的提问来源于stack exchange,提问作者WolverStone

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 20:06:22