hydrateRoot
在服务端渲染的 HTML 上激活 React - SSR 应用的客户端启动
核心概述
hydrateRoot 用于在服务端渲染的 HTML 上"激活" (hydrate) React, 使其具有交互性。与服务端渲染 API (renderToPipeableStream, renderToReadableStream) 配合使用, 实现流式服务端渲染 + 客户端 hydration的完整 SSR 流程。
Hydration vs 渲染:
- 渲染:从头创建 DOM,用于纯客户端应用 (createRoot)
- Hydration:复用服务端生成的 HTML,仅添加事件监听器,更快且保留 SEO
重要:Hydration 要求服务端和客户端渲染的 HTML 完全一致, 否则会触发 hydration mismatch 警告。
🌊 流式 SSR + Hydration
服务端:renderToPipeableStream → 流式传输 HTML
客户端:hydrateRoot → 激活交互性
技术规格
导入方式
TypeScript
import { hydrateRoot } from 'react-dom/client';函数签名
TypeScript
function hydrateRoot(
container: Element | DocumentFragment,
children: ReactNode,
options?: {
onRecoverableError?: (error: Error) => void;
identifierPrefix?: string;
}
): Root参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
container | Element | 包含服务端渲染 HTML 的 DOM 容器 |
children | ReactNode | 要 hydrate 的 React 元素,应与服务端渲染的内容一致 |
实战演练
1. 基础用法
TypeScript
// 客户端入口文件 (client.js 或 client.tsx)
import { hydrateRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
// Hydrate 服务端渲染的 HTML
hydrateRoot(container, <App />);2. 完整的 SSR 应用的客户端启动
TypeScript
// client.tsx
import { hydrateRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const container = document.getElementById('root');
if (!container) {
throw new Error('Failed to find the root element');
}
hydrateRoot(container, <App />, {
onRecoverableError(error) {
console.error('Hydration error:', error);
// 可以发送到错误监控服务
// sendToErrorTracking(error);
},
});
// 检测 hydration 错误
window.addEventListener('error', (event) => {
if (event.message.includes('Hydration')) {
console.error('Hydration mismatch detected:', event);
}
});3. 完整的 SSR 示例 (服务端 + 客户端)
TypeScript
// ========== 服务端 (server.js) ==========
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';
app.get('/', (req, res) => {
const stream = renderToPipeableStream(<App url={req.url} />, {
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-type', 'text/html');
res.write('<!DOCTYPE html><html><head><title>SSR App</title></head>');
res.write('<body><div id="root">');
stream.pipe(res);
},
bootstrapModules: ['/client.js'], // 客户端入口
});
stream.on('end', () => {
res.end('</div></body></html>');
});
});
// ========== 客户端 (client.js) ==========
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root'), <App />);4. 处理 Suspense 的 Hydration
TypeScript
import { hydrateRoot } from 'react-dom/client';
import { Suspense } from 'react';
import App from './App';
hydrateRoot(
document.getElementById('root'),
<Suspense fallback={null}>
<App />
</Suspense>
);避坑指南
陷阱 1:Hydration Mismatch
问题:服务端和客户端渲染的 HTML 不一致。
TypeScript
// ❌ 错误:使用会改变渲染的 API
function App() {
const now = Date.now(); // 服务端和客户端时间不同
return <div>Current time: {now}</div>;
}
// ✅ 正确:使用 useEffect 处理客户端特有逻辑
function App() {
const [now, setNow] = useState(null);
useEffect(() => {
setNow(Date.now());
}, []);
return <div>Current time: {now || 'Loading...'}</div>;
}陷阱 2:使用只在浏览器可用的数据
TypeScript
// ❌ 错误
function App() {
const width = window.innerWidth; // 服务端没有 window 对象
return <div>Width: {width}</div>;
}
// ✅ 正确
function App() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <div>Width: {width}</div>;
}API 对比
| API | 用途 | 场景 |
|---|---|---|
createRoot | 从头渲染应用 | 纯客户端应用 (CSR) |
hydrateRoot | 激活服务端 HTML | 服务端渲染应用 (SSR) |
render | React 17 API (已弃用) | 旧项目,不推荐 |
hydrate | React 17 SSR API (已弃用) | 旧项目,不推荐 |