一条“Hydration failed”的错误,让我损失了半天时间
背景
我在用 Next.js App Router + Redux 开发一个任务管理应用,一切顺利,直到打开了 SSR(服务端渲染),突然看到这个令人头皮发麻的报错:
Hydration failed because the server rendered HTML didn't match the client.
报错定位在 <Header />
,但实际问题比看上去复杂得多。
问题原因
React SSR 会先在服务端输出 HTML,再在浏览器执行 hydration,如果服务端和客户端渲染的 DOM 结构不一致,就会报错。
Redux 的状态通常在客户端初始化,比如 auth.isAuthenticated
,在 SSR 阶段它通常是 false
,但客户端可能已经是 true
。
举个例子:
{isAuthenticated ? (
<span>欢迎,{user?.username}</span>
) : (
<Link href=\"/login\">登录</Link>
)}
服务端输出:
<a href=\"/login\">登录</a>
客户端切换成:
<span>欢迎,Luke</span>
标签类型完全不一样,React hydration 直接炸了。
修复方式:引入 useIsClient
import { useEffect, useState } from 'react';
export function useIsClient() {
const [isClient, setIsClient] = useState(false);
useEffect(() => setIsClient(true), []);
return isClient;
}
然后在你的组件中:
const isClient = useIsClient();
if (!isClient) return null; // 跳过 SSR 阶段
return isAuthenticated ? (
<span>欢迎,{user?.username}</span>
) : (
<Link href=\"/login\">登录</Link>
);
这样 React SSR 阶段就不会渲染出错误结构,hydration 就能成功。
更进一步:封装 ClientOnly 组件
function ClientOnly({ children }: { children: React.ReactNode }) {
const isClient = useIsClient();
if (!isClient) return null;
return <>{children}</>;
}
任何你想跳过 SSR 的组件,只需要这样写:
<ClientOnly>
<Header />
</ClientOnly>
结语
React SSR + Redux 状态管理配合 App Router 使用,确实不够“傻瓜式”。但一旦你掌握了客户端条件渲染与状态保护的技巧,这种问题就能快速应对。