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

Next.js响应式渲染组件引发Hydration失败问题求助

解决Next.js中Hydration不匹配的问题

问题背景

我用Next.js开发Header组件,原代码如下:

// 原header.tsx代码
import Image from 'next/image';
import { HeaderMenu } from './menu/headerMenu';
import { HamburgerButtons, HamburgerMenu } from './menu/hamburgerMenu';
import { useState } from 'react';
import SearchForm from './searchForm';
import classNames from 'classnames';
import SmartLink from './links';

const Header = ({ isFixed }: { isFixed?: boolean }) => {
  const [isHamburgerOpen, toggleHamburger] = useState<boolean>(false);
  const toggleHamburgerMenu = (): void => {
    isHamburgerOpen ? toggleHamburger(false) : toggleHamburger(true);
  }

  return (
    <header className={classNames(isFixed && 'fixed')}>
      <SmartLink href="/">
        <Image src="/logos/eeat-low-resolution-logo-color-on-transparent-background.svg" alt="Logo" width="70" height="85"/>
      </SmartLink>

      <HeaderMenu/>

      <div className="flex flexItemsCenter">
        <SearchForm placeholder="SEARCH..."/>
        <HamburgerButtons onClick={toggleHamburgerMenu} isHamburgerOpen={isHamburgerOpen}/>
      </div>

      <HamburgerMenu isHamburgerOpen={isHamburgerOpen}/>
  </header>
  )
};
export default Header;

为了在桌面端(视窗宽度≥992px)隐藏汉堡按钮和菜单,我引入react-responsive的useMediaQuery做判断,修改后的代码:

// 修改后的header.tsx
import Image from 'next/image';
import { useMediaQuery } from 'react-responsive';
import { HeaderMenu } from './menu/headerMenu';
import { HamburgerButtons, HamburgerMenu } from './menu/hamburgerMenu';
import { useState } from 'react';
import SearchForm from './searchForm';
import classNames from 'classnames';
import SmartLink from './links';

const Header = ({ isFixed }: { isFixed?: boolean }) => {
  const [isHamburgerOpen, toggleHamburger] = useState<boolean>(false);
  const toggleHamburgerMenu = (): void => {
    isHamburgerOpen ? toggleHamburger(false) : toggleHamburger(true);
  }

  const isDesktop = useMediaQuery({ query: '(min-width: 992px)' });

  return (
    <header className={classNames(isFixed && 'fixed')}>
        <SmartLink href="/">
          <Image src="/logos/eeat-low-resolution-logo-color-on-transparent-background.svg" alt="Logo" width="70" height="85"/>
        </SmartLink>

        <HeaderMenu/>

        <div className="flex flexItemsCenter">
          <SearchForm placeholder="SEARCH..."/>
          {!isDesktop && <HamburgerButtons onClick={toggleHamburgerMenu} isHamburgerOpen={isHamburgerOpen}/>}
        </div>

        {!isDesktop && <HamburgerMenu isHamburgerOpen={isHamburgerOpen}/>}
    </header>
  )
};
export default Header;

汉堡组件代码:

// hamburgerMenu.tsx
import classnames from 'classnames'
import { HeaderMenu } from './headerMenu'

export const HamburgerButtons = ({ onClick, isHamburgerOpen }: { onClick: () => void, isHamburgerOpen: boolean }) => (
    <div id="HamburgerButtons" className={classnames(isHamburgerOpen && 'hamburgerOpen')} onClick={onClick}>
        <span></span>
        <span></span>
        <span></span>
    </div>
)

 export const HamburgerMenu = ({ isHamburgerOpen }: { isHamburgerOpen: boolean }) => {
   return (
        <div id="Hamburger" className={classnames(isHamburgerOpen && 'hamburgerMenuOpen')}>
            <HeaderMenu id="HamburgerHeaderMenu"/>
        </div>
   )
 }

现在在桌面尺寸下刷新页面会报错:

Error: Hydration failed because the initial UI does not match what was rendered on the server.

但移动端尺寸启动页面一切正常,请问如何解决?


错误原因

核心问题是服务端渲染(SSR)时没有浏览器的window对象,react-responsive的useMediaQuery在服务端无法获取真实视窗尺寸,会返回默认值(通常为false);而客户端能正确读取视窗宽度,导致服务端渲染的DOM结构和客户端hydrate时的DOM结构不一致,触发Hydration不匹配错误。


解决方案

方案1:动态导入组件并禁用SSR

用Next.js的next/dynamic动态导入汉堡相关组件,设置ssr: false让组件只在客户端渲染,避免服务端和客户端的判断差异:

import Image from 'next/image';
import { useState, useEffect } from 'react';
import { HeaderMenu } from './menu/headerMenu';
import SearchForm from './searchForm';
import classNames from 'classnames';
import SmartLink from './links';
import dynamic from 'next/dynamic';

// 动态导入汉堡组件,禁用SSR
const HamburgerButtons = dynamic(() => import('./menu/hamburgerMenu').then(mod => mod.HamburgerButtons), { ssr: false });
const HamburgerMenu = dynamic(() => import('./menu/hamburgerMenu').then(mod => mod.HamburgerMenu), { ssr: false });

const Header = ({ isFixed }: { isFixed?: boolean }) => {
  const [isHamburgerOpen, toggleHamburger] = useState<boolean>(false);
  const [isDesktop, setIsDesktop] = useState(false);

  const toggleHamburgerMenu = (): void => {
    toggleHamburger(!isHamburgerOpen);
  };

  // 在客户端挂载后判断视窗尺寸
  useEffect(() => {
    const checkIsDesktop = () => {
      setIsDesktop(window.matchMedia('(min-width: 992px)').matches);
    };
    // 初始化判断
    checkIsDesktop();
    // 监听视窗变化
    window.addEventListener('resize', checkIsDesktop);
    return () => window.removeEventListener('resize', checkIsDesktop);
  }, []);

  return (
    <header className={classNames(isFixed && 'fixed')}>
        <SmartLink href="/">
          <Image src="/logos/eeat-low-resolution-logo-color-on-transparent-background.svg" alt="Logo" width="70" height="85"/>
        </SmartLink>

        <HeaderMenu/>

        <div className="flex flexItemsCenter">
          <SearchForm placeholder="SEARCH..."/>
          {!isDesktop && <HamburgerButtons onClick={toggleHamburgerMenu} isHamburgerOpen={isHamburgerOpen}/>}
        </div>

        {!isDesktop && <HamburgerMenu isHamburgerOpen={isHamburgerOpen}/>}
    </header>
  )
};
export default Header;

方案2:用useEffect延迟视窗判断

不依赖react-responsive,在客户端组件挂载后用window.matchMedia判断视窗尺寸,确保只在客户端执行判断:

// 替换header.tsx中原有的useMediaQuery代码
const [isDesktop, setIsDesktop] = useState(false);

useEffect(() => {
  const mediaQuery = window.matchMedia('(min-width: 992px)');
  // 初始化设置
  setIsDesktop(mediaQuery.matches);
  // 监听视窗变化
  const handleResize = () => setIsDesktop(mediaQuery.matches);
  mediaQuery.addEventListener('change', handleResize);
  return () => mediaQuery.removeEventListener('change', handleResize);
}, []);

服务端渲染时isDesktop初始值为false,组件挂载后会立刻更新为客户端实际的视窗状态,虽然会有一次重渲染,但不会触发Hydration错误。

方案3:App Router下使用'use client'指令

如果项目用Next.js 13+的App Router,给Header组件加上'use client'标记,让组件完全在客户端渲染,此时react-responsive可以正常工作:

'use client'; // 标记为客户端组件

import Image from 'next/image';
import { useMediaQuery } from 'react-responsive';
import { HeaderMenu } from './menu/headerMenu';
import { HamburgerButtons, HamburgerMenu } from './menu/hamburgerMenu';
import { useState } from 'react';
import SearchForm from './searchForm';
import classNames from 'classnames';
import SmartLink from './links';

const Header = ({ isFixed }: { isFixed?: boolean }) => {
  const [isHamburgerOpen, toggleHamburger] = useState<boolean>(false);
  const toggleHamburgerMenu = (): void => {
    toggleHamburger(!isHamburgerOpen);
  };

  // 客户端组件中useMediaQuery可以正常工作
  const isDesktop = useMediaQuery({ query: '(min-width: 992px)' });

  return (
    <header className={classNames(isFixed && 'fixed')}>
        <SmartLink href="/">
          <Image src="/logos/eeat-low-resolution-logo-color-on-transparent-background.svg" alt="Logo" width="70" height="85"/>
        </SmartLink>

        <HeaderMenu/>

        <div className="flex flexItemsCenter">
          <SearchForm placeholder="SEARCH..."/>
          {!isDesktop && <HamburgerButtons onClick={toggleHamburgerMenu} isHamburgerOpen={isHamburgerOpen}/>}
        </div>

        {!isDesktop && <HamburgerMenu isHamburgerOpen={isHamburgerOpen}/>}
    </header>
  )
};
export default Header;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 20:23:10