Skip to content

react-dom、react-dom/client 与 react-dom/server

很多人学 React 时,默认只会接触:

js
import ReactDOM from "react-dom";

但随着 React 版本演进和渲染模式变化,react-dom 这条生态线其实已经拆得更清楚了。

这篇文章重点讲:

  1. react-dom 到底在 React 体系里负责什么
  2. react-dom/clientreact-dom/server 分别做什么
  3. createRoothydrateRootrenderToString 等 API 的位置
  4. CSR、SSR、水合为什么都离不开它

1. react-dom 在 React 体系里负责什么

React 本体更偏:

  1. 组件模型
  2. 状态与更新模型
  3. React Element
  4. 调和和 Fiber

react-dom 更偏,把 React 的更新结果真正接到浏览器 DOM 或服务端输出上。

把分工拉开:

  1. react 负责“描述 UI 和更新 UI 的规则”
  2. react-dom 负责“把这套规则接到浏览器 DOM”

🌟 所以 React 本身不是浏览器专属框架,react-dom 才是它和浏览器 DOM 之间的重要桥梁。


2. 为什么后来会拆成 react-dom/clientreact-dom/server

因为客户端挂载和服务端渲染,本来就是两条不同主线。

所以 React 后来把职责拆得更清楚:

主要职责
react-dom/client浏览器端创建根节点、挂载、更新、水合
react-dom/server服务端把 React 树转成 HTML 字符串或流

这样做的好处是:

  1. 职责更清楚
  2. API 归属更自然
  3. 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 />);

这段代码最关键的事情是:

  1. 找到浏览器里的一个真实 DOM 容器
  2. 创建 React 根节点
  3. <App /> 这棵 React 树渲染进去

3.2 root.render

root.render(...) 不是“简单塞 HTML”,而是把一棵 React 树交给 React 的渲染、调和、提交流程处理。


4. hydrateRoot 在解决什么问题

hydrateRoot 主要用于 SSR 场景下的水合。

把过程摊开看:

  1. 服务端已经先给浏览器一份 HTML
  2. 浏览器把内容显示出来
  3. 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 字符串。

适合先帮助理解:

  1. 服务端渲染不是浏览器执行组件
  2. 服务端也能执行 React 组件并产出 HTML

5.2 renderToPipeableStream

更现代的服务端渲染,往往会采用流式输出。

在 Node 环境里,更常见的是:

jsx
import { renderToPipeableStream } from "react-dom/server";
import App from "./App";

const { pipe } = renderToPipeableStream(<App />);

它的价值通常是:

  1. 更早开始把 HTML 片段返回给浏览器
  2. 减少必须“整页算完再一次性返回”的等待
  3. 更适合复杂页面和流式 SSR

6. CSR、SSR 和水合怎么串起来理解

可以按这条线记:

6.1 CSR

客户端渲染里,更常见的是:

  1. 服务端先返回一个 HTML 壳
  2. 浏览器加载 JS
  3. createRoot(...).render(...) 接管页面

6.2 SSR

服务端渲染里,更常见的是:

  1. 服务端先用 react-dom/server 产出 HTML
  2. 浏览器看到页面内容
  3. 客户端再用 hydrateRoot(...) 水合

6.3 两者的关键差异

不是“最后都显示页面”,而是:

  1. HTML 是谁先生成的
  2. HTML 是什么时候生成的
  3. 浏览器拿到 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 />);

这条链路里最重要的几个动作是:

  1. 服务端先生成 HTML
  2. 浏览器先显示内容
  3. 客户端再把事件和状态接回去

8. react-dom 这条线最常见的误区

8.1 以为 React 就等于浏览器 DOM

不是。

React 是更新模型,react-dom 才是浏览器宿主接入层。

8.2 以为 SSR 就是“在服务器先跑一遍页面”

说法太粗。

更贴近实际的过程是:服务端把 React 树转成 HTML,再由客户端水合接管。

8.3 以为 hydrateRootcreateRoot 只是名字不同

不是。

一个是客户端从零挂载,一个是客户端接管已有服务端 HTML。


9. 一句话总结

react-dom 这条生态主线,本质上是在解决 React 的组件和更新模型如何接到真实宿主环境里,而 react-dom/client 负责客户端挂载与水合,react-dom/server 负责服务端产出 HTML。

基于 VitePress 构建的个人技术笔记。