Server Components

React 19 のサーバーで実行されるコンポーネントで、JS バンドルサイズを削減しパフォーマンスを向上させる

Reactフロントエンド

Server Components とは

React Server Components (RSC) は、サーバーで実行されるコンポーネントで、JavaScript がクライアントに送信されない。DB アクセスや API キーの使用がサーバーで安全に行え、バンドルサイズが削減される。Next.js App Router ではデフォルトで全コンポーネントが Server Component であり、対話が必要な部品だけを明示的にクライアント側へ切り出す形になる。

設計上の要点は「どちらで動かすか」ではなく「境界をどこに引くか」にある。境界の内側と外側は別の環境で実行されるため、値の渡し方に直列化の制約が生まれ、コードの分割単位もその制約に従うことになる。

Server vs Client Component

2 つの差は実行場所だけではない。クライアントへ JavaScript が送られるかどうかで、使える React の機能も、扱ってよい機密情報も変わる。

観点Server ComponentClient Component
実行場所サーバーブラウザ
JS バンドル含まれない含まれる
DB アクセス直接できるAPI 経由で取得する
useState / useEffect使えない使える
イベントハンドラ使えない使える
宣言デフォルト'use client'

使い分け

Server Component はサーバーで実行されるため、DB アクセスやファイル読み取りを直接行える。Client Component はブラウザで実行され、useStateonClick などのインタラクティブな機能を使う。データ取得が主な処理は Server Component、ユーザー操作を伴う処理は Client Component を選ぶ。

// app/users/page.tsx (ディレクティブ無し = Server Component)
import { SearchBox } from './search-box';

async function UserList() {
  const users = await db.users.findMany(); // サーバーで直接 DB アクセス
  return (
    <ul>
      {users.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

export default async function Page() {
  return (
    <div>
      <SearchBox />       {/* 別ファイルの Client Component */}
      <UserList />        {/* 同じファイルの Server Component */}
    </div>
  );
}
// app/users/search-box.tsx
'use client';

import { useState } from 'react';

export function SearchBox() {
  const [query, setQuery] = useState('');
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

対話部品をわざわざ別ファイルにしているのは体裁の問題ではない。'use client' はファイルの先頭に置いて初めて効き、そのファイルと、そこから import した依存も含めてクライアント側のコードになる。1 つのファイルに両方を書き並べて途中でディレクティブを挟む書き方は成立しない。

シリアライズ境界

Server Component から Client Component へ渡す props は、直列化できる値でなければならない。文字列・数値・bigint・真偽値・null・undefined、グローバル登録された Symbol、配列や Map / Set、Date、TypedArray、素のオブジェクト、Promise、Server Function、そして Client / Server Component の要素 (JSX) は渡せる。渡せないのは、クライアント側のモジュールから export されておらず Server Function でもない関数、クラスそのものとクラスのインスタンス、プロトタイプを持たないオブジェクト、グローバル登録していない Symbol である。

この制約は「サーバーでの実行結果だけがネットワークを越える」という設計から出てくる。ORM が返したモデルインスタンスやコールバック関数をそのまま props に載せる設計は境界で破綻するため、渡す直前に必要なフィールドだけの素のオブジェクトへ落とす。落とし穴になりやすいのは、Date だけを見て「そのまま渡せる」と一般化してしまう場合で、同じ日時でも独自クラスで包んでいれば通らない。

境界は逆方向にも効く。Client Component の中で別のコンポーネントを import すると、それがサーバー専用の処理を含んでいてもクライアント側のコードとして扱われる。サーバーでしか動かない子を対話的な親の内側に置きたい場合は、親の中で import するのではなく children として受け取り、組み立てはサーバー側で行う。

バンドルサイズへの影響

送信される JavaScript の内訳が、従来の SSR とは根本的に違う。

従来の SSR:
  サーバーで HTML 生成 → クライアントに全 JS を送信 → Hydration

Server Components:
  サーバーで HTML 生成 → Client Component の JS のみ送信
  Server Component の JS は送信されない → バンドルサイズ削減

Server Actions

Server Actions (React の API 名では Server Function) は、クライアントのコードから呼び出せるサーバー側の関数である。呼び出しは常にネットワーク越しの要求になるため、宣言できるのは async 関数だけで、引数と戻り値は直列化されて往復する。引数として渡せる型は Client Component への props とほぼ同じだが、FormData が加わり、JSX は渡せない。

// app/users/actions.ts (ファイル全体の export を Server Function として公開)
'use server';

export async function createUser(formData: FormData) {
  // 引数はクライアント由来 = 信頼できない入力として扱う
  const session = await getSession();
  if (!session?.canCreateUser) throw new Error('forbidden');
  await db.users.create({
    name: formData.get('name') as string,
    email: formData.get('email') as string,
  });
}
// app/users/create-user-form.tsx
'use client';

import { createUser } from './actions';

export function CreateUserForm() {
  return (
    <form action={createUser}>
      <input name="name" />
      <input name="email" />
      <button type="submit">Create</button>
    </form>
  );
}

ディレクティブの置き場所には意味がある。'use server' は async 関数の本体の先頭に書いてもよいが、その形の関数はサーバー側のファイルに置いたまま props として渡す使い方に限られる。クライアント側のファイルから import して呼ぶなら、上のようにファイル先頭でモジュール全体に宣言する必要がある。関数の途中や import より後ろに書いても効かず、バックティックの文字列も認識されない。

もう 1 つ、'use server' は「この関数をサーバー用にする」印ではない。既定でサーバー側にあるコードに対して、クライアントから呼べる入口を開ける宣言である。つまり公開エンドポイントが 1 つ増えるのと同じであり、フォームの画面を出さずに直接叩かれる前提で認可と入力検証をこの関数の中に置く。呼び出し側の画面でボタンを隠しても防御にはならない。

判断基準

迷ったときは、その部品が状態やイベントを必要とするかで切り分ける。Server Component は一度描画されたら再描画されず状態も持たないため、状態が要るなら境界の外側 (クライアント側) に置くしかない。

コンポーネント推奨
データ取得、DB アクセスServer Component
静的な表示Server Component
フォーム、ボタン、入力Client Component
useState, useEffect が必要Client Component
重いライブラリ (チャート等)Client Component (lazy load)

境界の引き方を誤ると、削るはずだったバンドルが逆に増える。ページ最上位に 'use client' を置いてしまうと、そこから import した部品の一切がクライアント側の依存になり、Server Component の利点は消える。境界はできるだけ葉に近い側へ、状態を持つ部品の単位で引く。

この記事は役に立ちましたか?

関連用語

関連する記事