5.2 Server Components vs Client Components
5.1 で見た通り、 RSC を扱うアプリには Server Component と Client Component の 2 種類があります。どちらも書き方はほとんど同じ関数コンポーネントなのですが、走る場所と使える API が違います。この章では、 その 2 つの境界をどう描いていくかを見ていきましょう。
コード例は Next.js App Router を前提にしますが、ディレクティブや制約は React そのものの仕様になります。
5.2.1 2 種類のコンポーネント
まず全体像から押さえておきます。
図 5.2.1 Server / Client の境界
図 5.2.2 'use client' を書く位置と境界
'use client' をファイルの先頭に書くと、そこから先が「ブラウザに届く範囲」 になります。境界は「どのファイルに書くか」 で決まります。
それぞれの制約を、react.dev の Server Components のページから確認していきましょう。
5.2.2 Server Component の性質
App Router では、 app/ 以下の page.tsx や layout.tsx は 既定で Server Component です。ディレクティブは 何も書きません。
ここで一つ、間違えやすいポイントを公式から引いておきます。
A common misunderstanding is that Server Components are denoted by
"use server", but there is no directive for Server Components. The"use server"directive is used for Server Functions. (react.dev / Server Components)
Server Component に "use server" は不要です。"use server" は 5.4 で扱う Server Functions 用のディレクティブで、意味がまったく違います。ここは引っかかりやすいので、先に強調しておきますね。
Server Component は、コンポーネント関数そのものを async にして、await で直接データを取ってこられます。
// app/blog/page.tsx
// コンポーネント自体が async 関数
export default async function Page() {
const data = await fetch('https://api.example.com/posts');
const posts = await data.json();
return (
<ul>
{posts.map((p: { id: string; title: string }) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}サーバー側で走るので、fetch の代わりに ORM を直接呼ぶこともできます。
// app/blog/page.tsx
import { db, posts } from '@/lib/db';
export default async function Page() {
const allPosts = await db.select().from(posts);
return (
<ul>
{allPosts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}SPA では fetch('/api/posts') を経由していたものが、 API 層を置かずに DB まで直結できる、という点が Server Component の大きな違いです。
一方、 Server Component でできないことも react.dev にはっきり書かれています。
Server Components are not sent to the browser, so they cannot use interactive APIs like
useState.
つまり、 useState / useEffect / onClick / window / document は Server Component では使えません。インタラクションが要る部分は Client Component に分けていくことになります。
5.2.3 Client Component の性質
Client Component は、 ファイルの 先頭 に 'use client' を書きます。
// app/ui/counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
count: {count}
</button>
);
}'use client' は react.dev に次のように書かれています。
'use client'lets you mark what code runs on the client.(react.dev /'use client')
このディレクティブは サーバーとクライアントの境界 を作ります。もう少し具体的な説明も引いておきます。
When a file marked with
'use client'is imported from a Server Component, compatible bundlers will treat the module import as a boundary between server-run and client-run code.
つまり 'use client' は「ここから先は全部ブラウザで動かす」 という印です。そのファイルが import する他のファイルも、自動的に Client Component 扱いになります。Client Component では useState, useEffect, イベントハンドラ, window など、ブラウザの機能が使えます。
ただし、 Client Component の関数本体を async にすることはできません。 Client Component で非同期データを扱うには、後述する use API や、SWR / TanStack Query のようなライブラリを使うことになります(4.2, 7.3 参照)。
'use client' は再帰的に効く
Next.js の公式ドキュメントには次のような記述があります。
Once a file is marked with
"use client", all its imports and child components are considered part of the client bundle. This means you don't need to add the directive to every component that is intended for the client.(nextjs.org / Server and Client Components)
親ファイルに 'use client' を書けば、そのファイルが import する子コンポーネントにいちいちディレクティブを書かなくても、 Client Component として扱われます。
触って確かめる:境界ごとにバンドルはどう変わるか
'use client' をどこに書くかで、ブラウザに届く JS の範囲が変わる様子を、下のデモで切り替えてみてください。 LikeButton の 'use client' を付け外しすると、 「どのコンポーネントがブラウザ側に送られるか」 が色と数字で見えるようになっています。
LikeButtonは Client(onClickが使えます)SearchBoxはこの例では常に Client(キー入力を扱うため)- その他のコンポーネントは Server のまま
LikeButtonの JS がブラウザに送られますPostList/PostItemは Server なので、バンドルには入りません- Server → Client へは
propsとして serializable な値を渡します
※ これはページ内のデモで、実際の Next.js ビルドを再現しているわけではありません。 「どの境界を Client にすると、祖先ではなく自分と子孫がバンドルに入るか」 のイメージ用です。
「Client にした側だけが下流まで波及する」 感覚がつかめると、 「既定は Server、葉のほうだけ Client」 という設計方針(後述)が腑に落ちやすくなると思います。
5.2.4 境界は 1 度だけ越える
慣れないうちに起きがちなミスに、「Client Component の中で Server Component を import しようとする」 というのがあります。これは基本的にできません。Client Component からの import はクライアントに届く JS になるので、サーバー専用の処理(DB アクセスなど)を含むファイルを取り込むわけにはいかないからです。
では「Client Component の中に Server Component を入れたい」 ときはどうするかというと、答えは composition(children として渡す) です。
// app/ui/modal.tsx
'use client';
import { useState } from 'react';
// Client Component
export default function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>開く</button>
{open && <div className="modal">{children}</div>}
</>
);
}// app/page.tsx
import Modal from './ui/modal';
import Cart from './ui/cart'; // Server Component(例: DB から買い物かごを読む)
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
);
}Modal は Client Component ですが、 <Cart /> は親の Server Component である Page の中で評価されます。Modal からは 渡されてきた JSX として children を扱うだけなので、Server Component のコードをクライアントに持ち込むことにはなりません。
Next.js 公式の表現を借りると次の通りです。
In this pattern, all Server Components will be rendered on the server ahead of time, including those as props. The resulting RSC payload will contain references of where Client Components should be rendered within the component tree.
図 5.2.3 composition での入れ子
見た目の入れ子は Modal > Cart なのですが、 評価のオーナーは Page(Server)で、Cart もサーバーで実行されます。Modal は結果の JSX を children として受け取るだけ、という構図になっています。
5.2.5 境界の引き方の方針
実務では「どこで 'use client' を付けるか」 がアーキテクチャ判断になってきます。方針としては、次のルールが役に立ちます。
- 既定は Server。必要なところだけ Client にする。
- 画面全体を Client にしてしまうと、RSC の利点(バンドル削減・初期表示の改善)が消えてしまいます。
- Client Component は葉のほうに小さく作る。
- ボタン、入力、モーダルの開閉、アニメーションといった「触る」 単位を Client にし、データを持ってくる上位は Server にします。
- データは Server で取り、props として Client に渡す。
- Client から API をもう一度呼び直す必要はありません。
- ただし、props は serializable でなくてはいけない。
- 関数(イベントハンドラなど)を Server → Client に props で渡せるのは、Server Function に限られます(5.4 参照)。普通の関数を直接渡すとエラーになります。
- React Context は Client でしか使えない。
- Context Provider 自体を Client Component にして、
app/layout.tsxから<ThemeProvider>{children}</ThemeProvider>の形で差し込むのが定石です(4.4 参照)。
- Context Provider 自体を Client Component にして、
次の章では、 これらを具体的に Next.js App Router の上でどう書くかを見ていきます。