Appearance
react-dom、react-dom/client 与 react-dom/server
很多人学 React 时,默认只会接触:
js
import ReactDOM from "react-dom";但随着 React 版本演进和渲染模式变化,react-dom 这条生态线其实已经拆得更清楚了。
这篇文章重点讲:
react-dom到底在 React 体系里负责什么react-dom/client和react-dom/server分别做什么createRoot、hydrateRoot、renderToString等 API 的位置- CSR、SSR、水合为什么都离不开它
1. react-dom 在 React 体系里负责什么
React 本体更偏:
- 组件模型
- 状态与更新模型
- React Element
- 调和和 Fiber
而 react-dom 更偏,把 React 的更新结果真正接到浏览器 DOM 或服务端输出上。
把分工拉开:
react负责“描述 UI 和更新 UI 的规则”react-dom负责“把这套规则接到浏览器 DOM”
🌟 所以 React 本身不是浏览器专属框架,react-dom 才是它和浏览器 DOM 之间的重要桥梁。
2. 为什么后来会拆成 react-dom/client 和 react-dom/server
因为客户端挂载和服务端渲染,本来就是两条不同主线。
所以 React 后来把职责拆得更清楚:
| 包 | 主要职责 |
|---|---|
react-dom/client | 浏览器端创建根节点、挂载、更新、水合 |
react-dom/server | 服务端把 React 树转成 HTML 字符串或流 |
这样做的好处是:
- 职责更清楚
- API 归属更自然
- CSR、SSR、水合更容易按流程理解
3. react-dom/client 主要在做什么
这条线主要看的就是,浏览器如何接管一棵 React 应用。
3.1 createRoot
在现代 React 客户端渲染里,最常见的入口是:
jsx
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);这段代码最关键的事情是:
- 找到浏览器里的一个真实 DOM 容器
- 创建 React 根节点
- 把
<App />这棵 React 树渲染进去
3.2 root.render
root.render(...) 不是“简单塞 HTML”,而是把一棵 React 树交给 React 的渲染、调和、提交流程处理。
4. hydrateRoot 在解决什么问题
hydrateRoot 主要用于 SSR 场景下的水合。
把过程摊开看:
- 服务端已经先给浏览器一份 HTML
- 浏览器把内容显示出来
- React 再把事件、状态和组件逻辑接管回来
例如:
jsx
import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";
hydrateRoot(document.getElementById("root"), <App />);这不是“重新渲染一遍空页面”,而是接管已有 HTML。
🌟 水合真正解决的是,让服务端先给出可展示 HTML,再让客户端把这棵页面重新接回可交互状态。
5. react-dom/server 主要在做什么
这条线主要看的就是,如何在服务端把 React 树变成 HTML 输出给客户端。
5.1 renderToString
最容易理解的服务端渲染入口是:
jsx
import { renderToString } from "react-dom/server";
import App from "./App";
const html = renderToString(<App />);它会把 React 树转成 HTML 字符串。
适合先帮助理解:
- 服务端渲染不是浏览器执行组件
- 服务端也能执行 React 组件并产出 HTML
5.2 renderToPipeableStream
更现代的服务端渲染,往往会采用流式输出。
在 Node 环境里,更常见的是:
jsx
import { renderToPipeableStream } from "react-dom/server";
import App from "./App";
const { pipe } = renderToPipeableStream(<App />);它的价值通常是:
- 更早开始把 HTML 片段返回给浏览器
- 减少必须“整页算完再一次性返回”的等待
- 更适合复杂页面和流式 SSR
6. CSR、SSR 和水合怎么串起来理解
可以按这条线记:
6.1 CSR
客户端渲染里,更常见的是:
- 服务端先返回一个 HTML 壳
- 浏览器加载 JS
createRoot(...).render(...)接管页面
6.2 SSR
服务端渲染里,更常见的是:
- 服务端先用
react-dom/server产出 HTML - 浏览器看到页面内容
- 客户端再用
hydrateRoot(...)水合
6.3 两者的关键差异
不是“最后都显示页面”,而是:
- HTML 是谁先生成的
- HTML 是什么时候生成的
- 浏览器拿到 HTML 后,是重新挂载,还是接管已有内容
7. 一个最小的 SSR + hydrate 思路示例
下面这个示意代码不追求完整框架能力,只是为了把链路看顺。
7.1 服务端
jsx
import express from "express";
import { renderToString } from "react-dom/server";
import App from "./App";
const app = express();
app.get("/", (req, res) => {
const appHtml = renderToString(<App />);
res.send(`
<!DOCTYPE html>
<html>
<body>
<div id="root">${appHtml}</div>
<script src="/client.js"></script>
</body>
</html>
`);
});7.2 客户端
jsx
import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";
hydrateRoot(document.getElementById("root"), <App />);这条链路里最重要的几个动作是:
- 服务端先生成 HTML
- 浏览器先显示内容
- 客户端再把事件和状态接回去
8. react-dom 这条线最常见的误区
8.1 以为 React 就等于浏览器 DOM
不是。
React 是更新模型,react-dom 才是浏览器宿主接入层。
8.2 以为 SSR 就是“在服务器先跑一遍页面”
说法太粗。
更贴近实际的过程是:服务端把 React 树转成 HTML,再由客户端水合接管。
8.3 以为 hydrateRoot 和 createRoot 只是名字不同
不是。
一个是客户端从零挂载,一个是客户端接管已有服务端 HTML。
9. 一句话总结
react-dom 这条生态主线,本质上是在解决 React 的组件和更新模型如何接到真实宿主环境里,而 react-dom/client 负责客户端挂载与水合,react-dom/server 负责服务端产出 HTML。