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 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つの質問
- その処理はブラウザで実行される必要があるか。 無ければサーバーに置く
- その値はブラウザに届いて困らないか。 困るなら渡さない
- 切り出せる一番小さい単位はどこか。 そこに
'use client'を書く
use client そのものの意味はuse client とは何かに、ブラウザへ届くものの中身はブラウザに何が届くかにまとめてあります。