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

Next.js+Tailwind报Prop `className` did not match警告如何解决

Next.js 搭配Tailwind出现Prop className did not match警告修复

问题特征

  • 控制台抛出Prop className did not match水合警告,报错栈指向next/link组件
  • 差异表现:服务端渲染的导航项带激活态类font-medium text-white bg-gray-900 rounded-r-lg,客户端水合后变成未激活态类text-gray-200 hover:bg-gray-700
  • 触发场景:通过useRouter获取路由路径,动态判断导航项激活状态切换Tailwind类名

根本原因

核心是路由判断逻辑的输入值在SSR和客户端水合阶段不一致:useRouter返回的asPath在服务端渲染时拿到的是请求的原始路径,客户端首次水合时可能还没完成路由解析、重定向跳转,加上如果用了includes这类模糊匹配规则,很容易出现两端激活态判断结果相反的问题。

常见错误写法

// 预定义的Tailwind样式常量
const inactiveCls = 'text-gray-200 hover:bg-gray-700'
const activeCls = 'font-medium text-white bg-gray-900 rounded-r-lg'
const baseLinkCls = 'px-3 py-2 text-sm rounded-r-lg flex items-center gap-2'

function NavItems() {
  const router = useRouter()
  return navList.map(item => (
    <Link key={item.href} href={item.href}>
      {/* 问题点:1. 用includes模糊匹配,根路径/会匹配所有路由;2. 没等路由就绪就判断 */}
      <a className={classNames(baseLinkCls, router.asPath.includes(item.href) ? activeCls : inactiveCls)}>
        {item.name}
      </a>
    </Link>
  ))
}

修复方案

1. Pages Router 场景(兼容旧版Next.js)

等待路由完全就绪后再做激活态判断,同时替换模糊匹配为精确匹配,避免误判:

function NavItems() {
  const router = useRouter()
  // 路由未加载完成时,统一用未激活样式渲染,保证SSR和首次客户端输出一致
  if (!router.isReady) {
    return navList.map(item => (
      <Link key={item.href} href={item.href}>
        <a className={classNames(baseLinkCls, inactiveCls)}>
          {item.name}
        </a>
      </Link>
    ))
  }

  // 嵌套路由需要高亮父菜单时,单独处理根路径的匹配逻辑
  const checkActive = (href) => {
    if (href === '/') return router.asPath === '/'
    return router.asPath.startsWith(href)
  }

  return navList.map(item => (
    <Link key={item.href} href={item.href}>
      <a className={classNames(baseLinkCls, checkActive(item.href) ? activeCls : inactiveCls)}>
        {item.name}
      </a>
    </Link>
  ))
}

2. App Router 场景(Next.js 13+ 推荐)

直接用usePathname钩子替代useRouter拿路径,这个钩子官方做了同构处理,SSR和客户端返回值完全一致,不会出现匹配偏差:

'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

function NavItems() {
  const pathname = usePathname()
  // App Router下Link不需要包裹a标签,className直接传Link即可
  return navList.map(item => (
    <Link 
      key={item.href} 
      href={item.href}
      className={classNames(baseLinkCls, pathname === item.href ? activeCls : inactiveCls)}
    >
      {item.name}
    </Link>
  ))
}

3. 临时兜底方案(不推荐长期用)

如果特殊场景下必须在首次渲染输出动态样式,可以给对应元素加suppressHydrationWarning属性,仅会抑制当前元素的水合警告,不会影响其他组件:

<a 
  suppressHydrationWarning
  className={classNames(baseLinkCls, isActive ? activeCls : inactiveCls)}
>
  {item.name}
</a>

额外排查项

  • 不要在渲染阶段直接调用window/document等仅客户端存在的API生成类名,SSR阶段这些API不存在会直接导致类名不一致,这类逻辑要放到useEffect里或者路由就绪后执行
  • 不要写动态拼接的Tailwind类名,比如text-${color}-500,Tailwind构建时无法扫描到动态拼接的类,会导致样式丢失同时可能出现类名不匹配,预定义完整类名常量再切换即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 05:54:26