React 服务器组件 - This feature is available in the latest Canary

服务器组件是一种新型组件,它在打包之前,在与客户端应用或 SSR 服务器分离的环境中提前渲染。

这个独立的环境就是 React 服务器组件中的“服务器”。服务器组件可以在构建时在 CI 服务器上运行一次,也可以使用 Web 服务器为每个请求运行。

注意

如何构建对服务器组件的支持?

虽然 React 19 中的 React 服务器组件是稳定的,并且在主要版本之间不会中断,但用于实现 React 服务器组件打包器或框架的基础 API 不遵循语义化版本,并且可能会在 React 19.x 的次要版本之间中断。

要支持 React 服务器组件作为打包器或框架,我们建议固定到特定的 React 版本,或使用 Canary 版本。我们将在未来继续与打包器和框架合作,以稳定用于实现 React 服务器组件的 API。

没有服务器的服务器组件

服务器组件可以在构建时运行以从文件系统读取或获取静态内容,因此不需要 Web 服务器。例如,您可能希望从内容管理系统读取静态数据。

如果没有服务器组件,通常使用 Effect 在客户端获取静态数据

// bundle.js
import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function Page({page}) {
const [content, setContent] = useState('');
// NOTE: loads *after* first page render.
useEffect(() => {
fetch(`/api/content/${page}`).then((data) => {
setContent(data.content);
});
}, [page]);

return <div>{sanitizeHtml(marked(content))}</div>;
}
// api.js
app.get(`/api/content/:page`, async (req, res) => {
const page = req.params.page;
const content = await file.readFile(`${page}.md`);
res.send({content});
});

这种模式意味着用户需要下载并解析额外的 75K(gzip 压缩后)库,并在页面加载后等待第二个请求来获取数据,仅仅是为了渲染在页面生命周期内不会更改的静态内容。

使用服务器组件,您可以在构建时渲染这些组件一次

import marked from 'marked'; // Not included in bundle
import sanitizeHtml from 'sanitize-html'; // Not included in bundle

async function Page({page}) {
// NOTE: loads *during* render, when the app is built.
const content = await file.readFile(`${page}.md`);

return <div>{sanitizeHtml(marked(content))}</div>;
}

然后,渲染的输出可以是服务器端渲染 (SSR) 为 HTML 并上传到 CDN。当应用程序加载时,客户端将不会看到原始的 Page 组件,或者用于渲染 Markdown 的昂贵库。客户端将只看到渲染的输出

<div><!-- html for markdown --></div>

这意味着内容在首次加载页面时可见,并且捆绑包不包括渲染静态内容所需的昂贵库。

注意

您可能会注意到上面的服务器组件是一个异步函数

async function Page({page}) {
//...
}

异步组件是服务器组件的一项新功能,允许您在渲染中使用 await

请参阅下面的 使用服务器组件的异步组件

带有服务器的服务器组件

服务器组件还可以在页面请求期间在 Web 服务器上运行,让您无需构建 API 即可访问数据层。它们在应用程序打包之前呈现,并且可以将数据和 JSX 作为 props 传递给客户端组件。

如果没有服务器组件,通常会在 Effect 中获取客户端上的动态数据

// bundle.js
function Note({id}) {
const [note, setNote] = useState('');
// NOTE: loads *after* first render.
useEffect(() => {
fetch(`/api/notes/${id}`).then(data => {
setNote(data.note);
});
}, [id]);

return (
<div>
<Author id={note.authorId} />
<p>{note}</p>
</div>
);
}

function Author({id}) {
const [author, setAuthor] = useState('');
// NOTE: loads *after* Note renders.
// Causing an expensive client-server waterfall.
useEffect(() => {
fetch(`/api/authors/${id}`).then(data => {
setAuthor(data.author);
});
}, [id]);

return <span>By: {author.name}</span>;
}
// api
import db from './database';

app.get(`/api/notes/:id`, async (req, res) => {
const note = await db.notes.get(id);
res.send({note});
});

app.get(`/api/authors/:id`, async (req, res) => {
const author = await db.authors.get(id);
res.send({author});
});

使用服务器组件,您可以读取数据并将其渲染在组件中

import db from './database';

async function Note({id}) {
// NOTE: loads *during* render.
const note = await db.notes.get(id);
return (
<div>
<Author id={note.authorId} />
<p>{note}</p>
</div>
);
}

async function Author({id}) {
// NOTE: loads *after* Note,
// but is fast if data is co-located.
const author = await db.authors.get(id);
return <span>By: {author.name}</span>;
}

然后,打包器将数据、渲染的服务器组件和动态客户端组件组合到一个捆绑包中。可选地,然后可以对该捆绑包进行服务器端渲染 (SSR) 以创建页面的初始 HTML。当页面加载时,浏览器不会看到原始的 NoteAuthor 组件;只有渲染的输出发送到客户端

<div>
<span>By: The React Team</span>
<p>React 19 is...</p>
</div>

可以通过从服务器重新获取服务器组件来使其动态化,在服务器上它们可以访问数据并再次渲染。这种新的应用程序架构将以服务器为中心的多页应用程序的简单“请求/响应”心智模型与以客户端为中心的单页应用程序的无缝交互性相结合,为您提供两全其美的优势。

向服务器组件添加交互性

服务器组件不会发送到浏览器,因此它们不能使用像 useState 这样的交互式 API。要向服务器组件添加交互性,您可以使用 "use client" 指令将它们与客户端组件组合。

注意

服务器组件没有指令。

一个常见的误解是服务器组件由 "use server" 表示,但服务器组件没有指令。"use server" 指令用于服务器操作。

有关更多信息,请参阅指令文档。

在以下示例中,Notes 服务器组件导入了一个 Expandable 客户端组件,该组件使用状态来切换其 expanded 状态

// Server Component
import Expandable from './Expandable';

async function Notes() {
const notes = await db.notes.getAll();
return (
<div>
{notes.map(note => (
<Expandable key={note.id}>
<p note={note} />
</Expandable>
))}
</div>
)
}
// Client Component
"use client"

export default function Expandable({children}) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<button
onClick={() => setExpanded(!expanded)}
>
Toggle
</button>
{expanded && children}
</div>
)
}

这是通过首先将 Notes 渲染为服务器组件,然后指示打包器为客户端组件 Expandable 创建包来实现的。在浏览器中,客户端组件会将服务器组件的输出作为 props 传递

<head>
<!-- the bundle for Client Components -->
<script src="bundle.js" />
</head>
<body>
<div>
<Expandable key={1}>
<p>this is the first note</p>
</Expandable>
<Expandable key={2}>
<p>this is the second note</p>
</Expandable>
<!--...-->
</div>
</body>

服务器组件中的异步组件

服务器组件引入了一种使用 async/await 编写组件的新方法。当你在异步组件中使用 await 时,React 会暂停并等待 promise 解析后再继续渲染。这适用于跨服务器/客户端边界,并支持 Suspense 流式传输。

你甚至可以在服务器上创建一个 promise,并在客户端 await 它

// Server Component
import db from './database';

async function Page({id}) {
// Will suspend the Server Component.
const note = await db.notes.get(id);

// NOTE: not awaited, will start here and await on the client.
const commentsPromise = db.comments.get(note.id);
return (
<div>
{note}
<Suspense fallback={<p>Loading Comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}
// Client Component
"use client";
import {use} from 'react';

function Comments({commentsPromise}) {
// NOTE: this will resume the promise from the server.
// It will suspend until the data is available.
const comments = use(commentsPromise);
return comments.map(commment => <p>{comment}</p>);
}

note 内容是页面渲染的重要数据,因此我们在服务器上 await 它。评论位于折叠下方,优先级较低,因此我们在服务器上启动 promise,并在客户端使用 use API 等待它。这将在客户端上暂停,而不会阻止 note 内容的渲染。

由于异步组件在客户端上不受支持,因此我们使用 use 来 await promise。