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

参数说明

参数类型说明
containerElement包含服务端渲染 HTML 的 DOM 容器
childrenReactNode要 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)
renderReact 17 API (已弃用)旧项目,不推荐
hydrateReact 17 SSR API (已弃用)旧项目,不推荐

延伸阅读