伺服器元件

React 伺服器元件

伺服器元件適用於 React 伺服器元件

伺服器元件是一種新型的元件,它會在打包之前,在與客戶端應用程式或 SSR 伺服器分離的環境中預先渲染。

這個獨立的環境就是 React 伺服器元件中的「伺服器」。伺服器元件可以在建置時在 CI 伺服器上執行一次,也可以使用網路伺服器針對每個請求執行。

備註

我如何建置對伺服器元件的支援?

雖然 React 19 中的 React 伺服器元件很穩定,並且在主要版本之間不會中斷,但用於實作 React 伺服器元件打包器或框架的底層 API 不遵循語義化版本控制,並且可能在 React 19.x 的次要版本之間中斷。

為了支援 React 伺服器元件作為打包器或框架,我們建議固定到特定的 React 版本,或使用 Canary 版本。我們將繼續與打包器和框架合作,在未來穩定用於實作 React 伺服器元件的 API。

沒有伺服器的伺服器元件

伺服器元件可以在建置時執行以從檔案系統讀取或擷取靜態內容,因此不需要網路伺服器。例如,您可能想要從內容管理系統讀取靜態資料。

如果沒有伺服器元件,通常會使用 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(壓縮後)程式庫,並在頁面載入後等待第二個請求來擷取資料,僅為了渲染在頁面生命週期內不會更改的靜態內容。

使用伺服器元件,您可以在建置時將這些元件渲染一次

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

請參閱下面的 具有伺服器元件的非同步元件

具有伺服器的伺服器元件

伺服器元件也可以在頁面請求期間在網路伺服器上執行,讓您可以存取資料層而無需建置 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 建立一個捆綁包來實現的。在瀏覽器中,客戶端元件會將伺服器元件的輸出作為屬性傳遞。

<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,然後在客戶端上等待它。

// 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 等待 promise。