Server と Client の選び方
迷ったら Server Component から書き始めて、状態やイベント、ブラウザ API が必要になった部分だけを Client Component へ切り出します。
既定は Server Component
App Router では、何も書かなければ Server Component です。これは「初期値がたまたまそうなっている」のではなく、そちらを既定にしたほうが良いからそうなっています。
だから判断は「Client Component にするかどうか」の一方向だけです。Server Component にする理由を探す必要はありません。
Client Component が必要になる4つの場合
公式ドキュメントが挙げているのは次の4つです。
| 必要になるもの | 例 |
|---|---|
| 状態とイベント | useState、onClick、onChange |
| ライフサイクル | useEffect |
| ブラウザ専用の API | window、localStorage、navigator |
| それらを使うカスタムフック | 上の3つを内側で使っているもの |
逆に、Server Component が向いているのは次の場合です。
- データベースや API から、元に近いところで取得する
- API キーやトークンを、ブラウザに出さずに使う
- ブラウザへ送る JavaScript を減らす
- 最初の表示を早くする
判断の順番
- Serverまず何も書かずに始める
App Router の既定は Server Component。理由を探す必要はない。
- Browser状態・イベント・ブラウザ API が要るか
要るなら、その部分だけを別ファイルに切り出して use client を書く。
- Serverデータ取得は移さない
取得はページ側に残し、結果を props で渡す。境界を上げない。
- まず Server Component として書く。何も書かない
- 押せるもの・変わるものが出てきたら、その部分だけ切り出す
- 切り出したファイルの先頭に
'use client'を書く - データ取得は移さない。サーバーに残したまま props で渡す
この順番で書くと、境界は自然に葉のほうへ寄ります。逆に、ページの先頭に 'use client' を書いてから始めると、あとで境界を下げ直す作業が必要になります。
位置が変わると、送るコードが変わる
同じ画面を2通りに書いて、配信される成果物を実際に検査しました。
ページ全体を Client Component にした場合
'use client'
import { useState } from 'react'
import { priceLabel } from '@/lib/format'
export default function Page() {
const [n, setN] = useState(1200)
return <button onClick={() => setN(n + 1)}>{priceLabel(n)}</button>
}
押せるボタンだけを Client Component にした場合
import { LikeButton } from '@/components/LikeButton'
import { priceLabel } from '@/lib/format'
export default function Page() {
return (
<div>
{priceLabel(1200)}
<LikeButton />
</div>
)
}
lib/format.ts の実装が、配信される JavaScript に含まれるかどうか。
| 書き方 | format.ts のコード |
|---|---|
| ページ全体を Client に | 含まれる |
| ボタンだけを Client に | 含まれない |
'use client' を書いたファイルから import したものは、実装ごとブラウザへ運ばれます。重い整形処理や日付ライブラリを使っている画面ほど、この差は大きくなります。
'use client' の数ではなく、位置が効きます。
データ取得は移さない
境界を下げても、データの取得まで一緒に下げてはいけません。
// ❌ ボタンのために全部をブラウザへ持っていった
'use client'
export default function Page() {
const [items, setItems] = useState([])
useEffect(() => { fetch('/api/items').then(...) }, [])
}
// ✅ 取得はサーバー、押す部分だけブラウザ
export default async function Page() {
const items = await db.item.findMany()
return <List items={items} action={<LikeButton />} />
}
上は、往復が1つ増え、API を1本作る必要があり、ADMIN_KEY のような値も使えなくなります。下は何も増えません。
「Client の中に Server を置きたい」と思ったら
それは境界の引き方を間違えているか、children で解決できるかのどちらかです。import では解決しません。
書き方はClient Component の中に Server Component を置くにあります。
迷ったときの3つの質問
| やりたいこと | どちら | 理由 |
|---|---|---|
| 一覧を DB から出す | Server | 取得も整形もサーバーで済む |
| ボタンで開閉する | Client | 状態が要る |
| フォームを送って保存する | Server | form の action に Server Function を渡す |
| 入力に応じて絞り込む | Client | 打つたびに反応が要る |
| 日付を整形して出す | Server | 重いライブラリをブラウザへ送らずに済む |
| 保存した設定を復元する | Client | localStorage は useEffect の中で読む |
- その処理はブラウザで実行される必要があるか。無ければサーバーに置く
- その値はブラウザに届いて困らないか。困るなら渡さない
- 切り出せる一番小さい単位はどこか。そこに
'use client'を書く
use client そのものの意味はuse client とは何かに、ブラウザへ届くものの中身はブラウザに何が届くかにまとめてあります。
出典
- nextjs.org/docs/app/getting-started/server-and-client-components
- nextjs.org/docs/app/api-reference/directives/use-client
このページの内容は上記の一次情報を基準に書いています。個人ブログや二次情報を根拠にしていません。