TL;DR
- Next.js 16系で RSC(React Server Components)は完全に stable。デフォルトは Server、
'use client'を付けたモジュール以下が Client という単純なルールだが、実装現場では「どこに付けるか」で毎回30分悩む。本記事はその思考時間を3秒に縮める判断テンプレを配布する(React 公式: Server Components / Next.js 公式: Server and Client Components) - 判定は 4軸スコアシート:データフェッチ起点(DB/API or ユーザ操作)/インタラクティブ性(イベント・state・effect)/バンドルサイズ(重いライブラリは Server に押し込む)/SEO(検索流入対象は Server で確実にHTML化)。3軸以上 Server 寄りなら Server、2軸以上 Client 寄りなら Client(経験則)
- 最頻アンチパターンは 「ルートレイアウトに
'use client'を付ける」「Client からfetchする」「shadcn/ui のラッパーを毎回 Client にする」 の3つ。これだけでCache Componentsの効果が半減する(Vercel ブログ: Partial Prerendering も参照) - Composition パターン(children として Server を Client の中に挿す)を覚えると、
'use client'は葉のコンポーネントだけで済むようになる。これが境界設計の核心 - 末尾に 境界判断テンプレ12項目 + FAQ 5問 + Cache Components / フロント性能優先順位記事への接続 を載せた。
'use client'を付ける前に1分読めば事故が減る
はじめに
こんにちは、みねです。
「App Router に移行して半年。'use client' をどこに付けるかで毎回チームが議論になる。プルリク数が増えるほど、誰かが意識せずにルートレイアウトに 'use client' を付けて、Cache Components の効果が消える」── Tech Lead から最近よく受ける相談です。
結論から言うと、判断軸を持っていないチームは、PRレビューで毎回ゼロから議論する ことになります。これは設計負債ではなく 意思決定負債 です。
本記事は、Next.js 16系(2026-05時点)の App Router で RSC を本番運用しているチーム向けに、4軸スコアシートで「迷わず3秒で決める」判断テンプレ を提供します。前提は React 19 系 + Next.js 16系。Pages Router は対象外です。
関連記事として、キャッシュ層の設計は Next.js Cache Components移行設計、性能改善の優先順位は フロント性能改善の正しい順序: LCP→INP→CLS、API 層の選定は tRPC を採用すべきか 型安全API設計の現実解 を参照してください。
§1. 復習:RSC と Client Component の役割
1-1. なぜ境界が必要になったか
RSC 以前の React は、全てのコンポーネントがブラウザで実行される モデルでした。サーバ側で HTML を作る SSR はあっても、最終的にブラウザで全ツリーが hydrate される。これは「初期表示までは速いが、JavaScript の評価時間が長くなる」という INP 悪化の根本原因でした(web.dev: INP)。
RSC は、「サーバでだけ実行されるコンポーネント」を React のレンダリングモデルに正規に組み込んだ 仕組みです。Client Component との違いは:
| 項目 | Server Component | Client Component |
|---|---|---|
| 実行場所 | サーバのみ | サーバ(初回HTML) + ブラウザ(hydrate以降) |
| バンドルへの影響 | 含まれない(公式値) | 含まれる |
| 使えるAPI | DB / fetch / 環境変数 / Node API | useState / useEffect / イベント / ブラウザAPI |
| 使えないAPI | useState / useEffect / onClick | DB直接 / Node-only API |
async | 直接 async function 可 | 不可(use() フック経由) |
ポイントは 「Server は JavaScript バンドルに含まれない」 こと。これが LCP / INP に効く本質的な理由です。
1-2. デフォルトは Server、'use client' で Client に切り替える
App Router では 全コンポーネントがデフォルトで Server Component です。'use client' directive を付けたファイルだけが Client Component に切り替わります(React 公式: 'use client' directive)。
// app/products/page.tsx
// directive なし → Server Component
export default async function ProductsPage() {
const products = await db.products.findMany()
return <ProductList items={products} />
}
// app/products/SearchFilter.tsx
'use client' // ← この一行で Client Component に切り替え
import { useState } from 'react'
export function SearchFilter() {
const [keyword, setKeyword] = useState('')
return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
}
「迷ったら Server」が原則です。理由は次の §2 で示します。
1-3. 'use client' の伝播範囲(モジュールグラフ)
最大の落とし穴は、'use client' を付けたファイルから import される全モジュールが Client 扱いになる ことです(公式値)。
// app/Layout.tsx
'use client' // ← ここに付けると…
import { Header } from './Header'
import { Footer } from './Footer'
import { ProductList } from './ProductList'
// Header / Footer / ProductList も全て Client 扱い
逆向きはOK:Server Component から Client Component を import するのは安全です。「Client は Client を伝染させるが、Server は伝染しない」 と覚えてください。
これが「ルートレイアウトに 'use client' を付けてはいけない」理由です。アプリ全体が Client 化し、RSC のメリットが消えます。
§2. 迷わない4軸判定
2-1. 軸1: データフェッチ起点
「そのデータは どこ起点 で取りに行くか?」を問います。
| 起点 | 判定 | 理由 |
|---|---|---|
| DB / 外部API(リクエスト時に1回) | Server | サーバで直接 await db.xxx.findMany() で済む(Next.js 公式: How to fetch data) |
| ユーザ操作(ボタン押下後) | Client | onClick 必須なので Client 一択 |
| ポーリング / WebSocket | Client | ライフサイクル管理が必須 |
| 環境変数 / シークレット | Server | クライアント漏洩を防ぐ |
最頻ケースは「ページ初回表示用データ → Server」「操作後の追加取得 → Client(または Server Action 経由で Server)」です。
2-2. 軸2: インタラクティブ性
onClick / onChange / useState / useEffect / ブラウザAPI(window, localStorage, IntersectionObserver 等)が必要なら Client 一択です。
// 必ず Client
'use client'
export function CartButton() {
return <button onClick={() => addToCart()}>カートに追加</button>
}
ただし 「インタラクティブな部分だけ」を Client 化 するのが核心です。ページ全体を Client 化してはいけません。
2-3. 軸3: バンドルサイズ
Markdown レンダラ・グラフライブラリ・コードハイライタ・PDF 生成など、100KB 超のライブラリは Server に押し込む のがセオリーです(経験則)。Client に置くと初期 JavaScript バンドルが膨らみ、INP が悪化します(web.dev: JavaScript の評価コスト)。
// Server Component(バンドル削減)
import { compileMDX } from '@/lib/mdx'
export default async function Article({ slug }: { slug: string }) {
const html = await compileMDX(slug) // 重い処理はサーバで
return <article>
}
判定基準:
| ライブラリサイズ | 判定 |
|---|---|
| 〜10KB | どちらでも可 |
| 10〜50KB | 可能なら Server |
| 50KB〜 | 原則 Server。Client にする場合は next/dynamic で遅延ロード必須 |
2-4. 軸4: SEO
検索流入対象の本文(記事本文、商品詳細、サービス紹介)は Server で確実にHTML化 します。Client Component は初期HTMLにも含まれますが、CSR 経由で挿入される DOM は Googlebot のクロール深度によっては読まれない場合があります(経験則)。
判定: 検索インデックスに乗せたい本文は Server。検索結果に出ない管理画面は Client でも問題なし。
2-5. 4軸スコアシート
flowchart TD
A[コンポーネント] --> B{軸1: データ起点}
B -->|DB/API| BS[Server +1]
B -->|ユーザ操作| BC[Client +1]
A --> C{軸2: インタラクティブ}
C -->|あり| CC[Client +1]
C -->|なし| CS[Server +1]
A --> D{軸3: バンドル50KB+}
D -->|あり| DS[Server +1]
D -->|なし| DN[どちらでも 0]
A --> E{軸4: SEO対象}
E -->|あり| ES[Server +1]
E -->|なし| EN[どちらでも 0]
BS --> F[合計]
BC --> F
CC --> F
CS --> F
DS --> F
DN --> F
ES --> F
EN --> F
F --> G{Server票 ≥ 3?}
G -->|Yes| GS[Server決定]
G -->|No| H{Client票 ≥ 2?}
H -->|Yes| HC[Client決定]
H -->|No| HS[Server デフォルト]
| 軸 | Server票 | Client票 |
|---|---|---|
| 1. データ起点 | DB/API なら +1 | ユーザ操作なら +1 |
| 2. インタラクティブ | なしなら +1 | ありなら +1 |
| 3. バンドル | 重いライブラリ依存なら +1 | — |
| 4. SEO | 検索対象なら +1 | — |
判定ルール(経験則):
- Server票 ≥ 3 → Server 決定
- Client票 ≥ 2 → Client 決定(インタラクティブ性が支配的)
- それ以外(迷う)→ Server デフォルト。後から Composition で Client を継ぎ足せばよい
§3. 5つの具体ケースで判定する
3-1. 商品一覧ページ → Server
// app/products/page.tsx
export default async function ProductsPage() {
const products = await db.products.findMany({ take: 20 })
return (
<div>
<h1>商品一覧</h1>
<ProductGrid items={products} />
</div>
)
}
判定: データ起点=DB(+S)、インタラクティブ=なし(+S)、バンドル=軽い(0)、SEO=対象(+S)。Server票3 → Server決定。
3-2. 商品検索フィルタ → Client
'use client'
import { useState, useTransition } from 'react'
import { searchProducts } from './actions'
export function SearchFilter() {
const [keyword, setKeyword] = useState('')
const [isPending, startTransition] = useTransition()
return (
<input
value={keyword}
onChange={(e) => {
setKeyword(e.target.value)
startTransition(() => searchProducts(e.target.value))
}}
/>
)
}
判定: データ起点=操作(+C)、インタラクティブ=あり(+C)、バンドル=軽い(0)、SEO=対象外(0)。Client票2 → Client決定。
ポイント: 検索結果データ自体は Server Action(searchProducts)で Server 取得 にする。Client から直接 fetch しない。
3-3. shadcn/ui の Button / Dialog → Client(推奨)
shadcn/ui のコンポーネントは内部で Radix UI を使っており、多くは Client Component です(公式値: shadcn/ui のソースに 'use client' 記載あり)。
ただし、ラッパーまで Client にする必要はない のが盲点です。
// NG: ラッパーを Client にしてバンドル肥大化
'use client'
import { Button } from '@/components/ui/button'
export function MyCard({ data }) {
return (
<div>
<h3>{data.title}</h3>
<Button>詳細</Button>
</div>
)
}
// OK: ラッパーは Server、Button だけ Client
import { Button } from '@/components/ui/button' // 内部で 'use client'
export function MyCard({ data }) { // ← Server のまま
return (
<div>
<h3>{data.title}</h3>
<Button>詳細</Button>
</div>
)
}
Server Component から Client Component を import するのは安全(§1-3 参照)。ラッパー側に 'use client' を付ける必要はありません。
3-4. Markdown レンダラ → Server(バンドル削減)
// Server Component
import { compileMDX } from 'next-mdx-remote/rsc'
export default async function Article({ source }: { source: string }) {
const { content } = await compileMDX({ source })
return <article>{content}</article>
}
判定: データ起点=ビルド時/サーバ(+S)、インタラクティブ=なし(+S)、バンドル=重い(+S)、SEO=対象(+S)。Server票4 → Server決定。
next-mdx-remote/rsc のように 「RSC 用」と明示されたエントリポイント を選ぶと、Client バンドルから完全に除外できます。
3-5. リアルタイムチャート → Client(必要悪)
'use client'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('chart.js/auto').then(m => m.Chart), { ssr: false })
export function RealtimeChart() {
// WebSocket でデータ受信、Chart 描画
}
判定: データ起点=WebSocket(+C)、インタラクティブ=あり(+C)、バンドル=重い(+S だが Client 必須)、SEO=対象外(0)。Client票2 → Client決定。
ポイント: バンドルが重いのは next/dynamic で遅延ロード することで初期表示への影響を最小化。SSR=false でクライアントのみ描画。
§4. アンチパターン5選
4-1. ルートレイアウトに 'use client' を付ける
最頻 Critical 級ミス。app/layout.tsx に 'use client' を付けると アプリ全体が Client 化 し、RSC のメリットが完全に消えます。Cache Components(Next.js Cache Components移行設計)の効果も無効化されます。
回避: ルートレイアウトは絶対に Server のまま。テーマプロバイダなど Client が必要なものは、プロバイダだけ Client にして children を渡す(後述 §5)。
4-2. Client Component から fetch する
// NG
'use client'
import { useEffect, useState } from 'react'
export function ProductList() {
const [items, setItems] = useState([])
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setItems)
}, [])
return <ul>{items.map(i => <li>{i.name}</li>)}</ul>
}
問題: 初期HTMLが空 → SEO に乗らない、Waterfall になる、Cache Components の対象外。
回避: データ取得は Server Component で完結。Client では受け取った props を表示するだけ。どうしても Client 起点で取得したいなら Server Action を経由。
4-3. Client 内で巨大 JSON を useState に保持
// NG
'use client'
export function ProductPage({ allProducts }) { // 10,000 件
const [products, setProducts] = useState(allProducts)
// ...
}
問題: 初期HTML に JSON 全件がシリアライズされ、hydration コストが爆発(INP 悪化)。
回避: 必要な分だけ Server で絞ってから Client に渡す。ページネーション or 仮想スクロール。
4-4. useEffect でデータ取得を再実装
useEffect でのデータ取得は React 19 系では アンチパターン とされています(React 公式: 'use client' の周辺ドキュメント参照)。Server Component で取得して props で渡す方が、コード量・パフォーマンス・SEO の全てで優位です。
回避: useEffect を見たら「Server に移せないか」を必ず疑う。
4-5. shadcn/ui のラッパーを毎回 Client にする
§3-3 で示した通り、ラッパー側に 'use client' を付ける必要はありません。ラッパーが Client 化すると、その配下の 全コンポーネントが Client 伝染 します(§1-3)。
回避: ラッパーは Server、Client が必要な葉だけ Client、を徹底。PR レビューで 'use client' の追加 PR は必ず「最小範囲か?」を確認。
§5. Composition パターン(Client の中に Server を入れる)
「Client コンポーネントの内側に Server コンポーネントを置きたい」場面は頻出します(テーマプロバイダの中に記事本文、Modal の中に動的コンテンツなど)。
直接 import するとモジュールグラフ伝染で Server も Client 化してしまいます。children として渡す のが解決策です(Next.js 公式: Server and Client Components の Composition Patterns 節)。
5-1. children として渡す
// ThemeProvider.tsx (Client)
'use client'
import { createContext } from 'react'
const ThemeContext = createContext('light')
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}
// app/layout.tsx (Server のまま)
import { ThemeProvider } from './ThemeProvider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ThemeProvider>
{children} {/* ← 中身は Server のまま */}
</ThemeProvider>
</body>
</html>
)
}
ポイント: ThemeProvider は Client だが、children プロパティ経由で渡された JSX は Server のまま です。これは React のレンダリングモデルが「props として渡された JSX は親の境界とは独立に評価される」設計だからです。
5-2. props として渡す(Slot パターン)
// Modal.tsx (Client)
'use client'
import { useState } from 'react'
export function Modal({ trigger, content }: { trigger: React.ReactNode; content: React.ReactNode }) {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>{trigger}</button>
{open && <div>{content}</div>}
</>
)
}
// app/page.tsx (Server)
import { Modal } from './Modal'
import { ProductDetail } from './ProductDetail' // Server Component
export default async function Page() {
const product = await db.products.findFirst()
return (
<Modal
trigger="詳細を開く"
content={<ProductDetail product={product} />} // ← Server を Client の props に
/>
)
}
これで Modal 自体は軽い Client、中身(重いデータ取得を伴う)は Server という理想形になります。
§6. 境界とキャッシュ/パフォーマンスの接続
6-1. Cache Components との連動
'use cache' directive は Server Component / Server 関数でのみ使える ことに注意。Client Component に付けても無効です(公式値)。
つまり「Server に押し込んだもの=キャッシュ可能になるもの」です。境界設計とキャッシュ設計は表裏一体です。詳細は Next.js Cache Components移行設計 を参照してください。
6-2. hydration コストと INP
Client Component が増えるほど、初期HTMLに含まれるシリアライズデータが増え、hydration の JavaScript 実行時間が長くなります。これは INP(Interaction to Next Paint)の直接的な悪化要因 です(web.dev: INP)。
性能改善の観点での優先順位は フロント性能改善の正しい順序: LCP→INP→CLS で詳述しています。本記事の境界設計は そもそも Client コードを最小化する ことで、INP 改善の上流で効きます。
6-3. Server Functions との関係
Server Action(React 公式: Server Functions)は、Client から Server へ書き込み・取得を呼び出す 仕組みです。Client Component で「データを取りたい」「更新したい」場面では、fetch ではなく Server Action を呼ぶ のが現代の正解です。
// app/actions.ts (Server)
'use server'
export async function searchProducts(keyword: string) {
return await db.products.findMany({ where: { name: { contains: keyword } } })
}
これで Client は「呼び出すだけ」になり、データ取得ロジックは Server に集約されます。
§7. 境界判断テンプレ 12項目(offer)
PR レビューで使えるチェックリストです。'use client' を含む PR で全項目に √ が付くまでマージしないルールが推奨です。
| # | 項目 | 判定基準 | 担当 |
|---|---|---|---|
| 1 | 'use client' は最小範囲か | ファイル単位で葉のコンポーネントだけ | FE |
| 2 | ルートレイアウトに 'use client' ない | app/layout.tsx に grep 0件 | FE |
| 3 | データ取得は Server で完結 | Client 内に fetch/axios 0件 | FE |
| 4 | useEffect でのデータ取得 0件 | grep 確認 | FE |
| 5 | 重いライブラリ(50KB+)は Server | next/dynamic or Server import | FE |
| 6 | shadcn/ui ラッパーは Server のまま | 'use client' の親 commit 確認 | FE |
| 7 | Client→Server は children/props 経由 | Composition パターン採用 | FE |
| 8 | Server Action で Client→Server 通信 | API Route 新規追加なし | FE |
| 9 | SEO 対象本文は Server | クロール対象の <article> 等 | FE / SEO |
| 10 | 巨大 JSON の Client 渡しなし | props サイズ < 100KB | FE |
| 11 | 'use cache' 対象は Server に集約 | Cache Components と連動確認 | FE |
| 12 | INP P75 < 200ms 維持 | Vercel Analytics or RUM | DevOps |
このチェックリストを PR description テンプレに貼り、各項目に √ を付けながら進めるのが最短ルートです。
§8. FAQ
Q1. Server Component で useState が使えない理由は?
useState は コンポーネントが「再レンダリング」される前提 の API です。Server Component はリクエストごとに1回だけサーバで実行され、ブラウザでは hydrate されないため、状態を保持する概念自体が存在しません(公式値)。状態が必要なら Client にするか、URL の Search Params / Cookie に永続化します。
Q2. 'use client' の伝播範囲はどこまで?
'use client' を付けたファイルから import される全モジュール が Client 扱いになります(公式値)。逆向きはOK:Server Component から Client Component を import するのは安全です。「Client は Client を伝染させるが、Server は伝染しない」と覚えてください。
Q3. Client Component の中に Server Component を入れられる?
直接 import する形ではできません。children または props として渡す Composition パターン で実現します(§5 参照)。これにより、テーマプロバイダや Modal の中身を Server のまま保てます。
Q4. データ取得は Client / Server どちらで書くべき?
初回表示用は Server、ユーザ操作後の取得は Server Action 経由で Server。Client Component の中で fetch を直接書くのはアンチパターンです(§4-2)。Server Action なら型安全で Cache Components の対象にもできます。
Q5. shadcn/ui や Radix UI は全て Client?
多くは内部で 'use client' が付いた Client Component です(公式値: shadcn/ui のソースに記載)。ただし 使う側のラッパーまで Client にする必要はありません。Server Component から Client Component を import するのは安全なので、ラッパーは Server のまま、葉だけ Client が原則です(§3-3 参照)。
まとめ
- RSC の境界設計は 意思決定負債 になりやすい。チームに判断軸がないと毎回ゼロから議論することになる
- 4軸スコアシート(データ起点 / インタラクティブ / バンドル / SEO) で3秒判定。Server票 ≥ 3 で Server、Client票 ≥ 2 で Client、迷ったら Server デフォルト
- アンチパターンの最頻3つは「ルートレイアウト
'use client'」「Client から fetch」「shadcn/ui ラッパー Client 化」。これだけで Cache Components の効果が半減する - Composition パターン(children/props で Server を Client に挿す)を覚えると、
'use client'は葉のコンポーネントだけで済む - 12項目チェックリストを PR テンプレに貼り、
'use client'を含む PR で全項目 √ までマージしない運用が現実解
'use client' を付ける前にこの記事を1分読み返してください。境界判断テンプレに沿うだけで PR レビューが10分短縮できます。境界設計の相談は みね(X) まで。
関連記事
- Next.js Cache Components移行設計 ── 境界の次は宣言的キャッシュ
- フロント性能改善の正しい順序: LCP→INP→CLS ── INP 悪化の上流で効く
- tRPC を採用すべきか 型安全API設計の現実解 ── Server Action と API 層の使い分け
- Claude Skills対Subagents使い分け5軸 ── 同 queue・5軸判断テンプレの先行事例
