サーバーコンポーネント

Reactサーバーコンポーネント

サーバーコンポーネントは、React Server Componentsで使用するためのものです。

サーバーコンポーネントは、クライアントアプリや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圧縮)のライブラリをダウンロードして解析し、2回目のリクエストでデータをフェッチするのを待つ必要があることを意味します。

サーバーコンポーネントを使用すると、これらのコンポーネントをビルド時に一度レンダリングできます。

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コンポーネントや、マークダウンをレンダリングするための高価なライブラリを表示しません。クライアントに表示されるのは、レンダリングされた出力のみです。

<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を作成できます。ページがロードされると、ブラウザは元のNoteおよびAuthorコンポーネントを表示しません。レンダリングされた出力のみがクライアントに送信されます。

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

サーバーコンポーネントは、サーバーから再フェッチすることで動的にすることができます。サーバーでは、データにアクセスして再度レンダリングできます。この新しいアプリケーションアーキテクチャは、サーバー中心のマルチページアプリのシンプルな「リクエスト/レスポンス」メンタルモデルと、クライアント中心のシングルページアプリのシームレスなインタラクティビティを組み合わせることで、両方の長所を提供します。

サーバーコンポーネントへのインタラクティビティの追加

サーバーコンポーネントはブラウザに送信されないため、useStateのようなインタラクティブなAPIを使用できません。サーバーコンポーネントにインタラクティビティを追加するには、"use client"ディレクティブを使用して、クライアントコンポーネントで構成できます。

注意

サーバーコンポーネントにディレクティブはありません。

よくある誤解として、サーバーコンポーネントは"use server"で示されると思われがちですが、サーバーコンポーネントにはディレクティブはありません。"use server"ディレクティブは、サーバー関数に使用されます。

詳細については、ディレクティブのドキュメントをご覧ください。

次の例では、Notesサーバーコンポーネントが、状態を使用してexpanded状態を切り替えるExpandableクライアントコンポーネントをインポートします。

// 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を作成し、クライアントで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でPromiseを待機します。