Next.js+Tailwind报Prop `className` did not match警告如何解决
Next.js 搭配Tailwind出现
Prop className did not match警告修复 问题特征
- 控制台抛出
PropclassNamedid 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
相关产品推荐
相关产品推荐

