所谓水合(Hydration),指的是 React 为预渲染的 HTML 添加事件处理程序,将其转为完全可交互的应用程序的过程。
React 提供了 hydrateRoot 客户端 API,通常搭配 react-dom/server 一起使用。先由 react-dom/server 生成 HTML,再调用 hydrateRoot 为生成的 HTML 进行水合,伪代码如下:
# 服务端
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />);
# 客户端
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
与我们使用 React 时常用的 createRoot 不同,creatRoot 会重新创建 DOM 节点,而 hydrateRoot 会尽可能复用已有的 DOM 节点。
那具体是怎么进行水合的呢?
这就要说到 React 的渲染原理了。你可以这样简单粗暴的理解:
当调用 hydateRoot 的时候,会传入组件(例子中的 ),React 会据此构建 React 组件树,并按照 组件树的顺序遍历真实的 DOM 树,判断 DOM 树和组件树是否对应,如何对应,则跳过创建 DOM 节点的环节,复用当前 DOM 节点,添加事件并进行关联。
所以水合的前提是 DOM 树和组件树渲染一致。
常见的水合错误
export default function App() {
return (
<p>
text1
<p>text2</p>
</p>
);
}
渲染时使用 typeof window !== 'undefined' 等判断
"use client";
export default function App() {
const isClient = typeof window !== "undefined";
return <h1>{isClient ? "Client" : "Server"}</h1>;
}
渲染时使用客户端 API 如 window、localStorage 等
"use client";
export default function App() {
return (
<h1>
{typeof localStorage !== "undefined" ? localStorage.getItem("name") : ""}
</h1>
);
}
"use client";
export default function App() {
return <h1>{Date.now()}</h1>;
}
解决方法
import { useEffect, useState } from "react";
export default function App() {
const [time, setTime] = useState(Date.now());
useEffect(() => {
const timer = setInterval(() => {
setTime(Date.now());
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return <h1>{time}</h1>;
}
为什么会渲染不一致呢?本质上还是客户端组件既在服务端也在客户端渲染一份,干脆取消掉客户端组件的服务端渲染,为此需要借助 Next.js 提供的 dynamic 函数。
import dynamic from "next/dynamic";
export default function App() {
const DynamicComponent = dynamic(() => import("./DynamicComponent"), {
ssr: false,
});
return <DynamicComponent />;
}
使用 suppressHydrationWarning 取消错误提示
export default function App() {
return (
<div suppressHydrationWarning>
{typeof window !== "undefined" ? "Client" : "Server"}
</div>
);
}