Server から Client へ渡せる値
Server Component から Client Component へ渡せるのは値だけで、関数とクラスのインスタンスは渡せません。
#なぜ制限があるのか
Server Component はサーバーで実行され、Client Component はブラウザにも送られます。その境界を props が越えるとき、値はネットワークを通れる形に変換されます。
関数やクラスのインスタンスは、値だけを運ぶと別物になってしまいます。React はそういう「一部だけ壊れた値」を作らず、境界で止めます。
#一覧
| 渡せる | 渡せない |
|---|---|
文字列・数値・真偽値・null | 関数 |
| 配列 | クラスのインスタンス |
| プレーンなオブジェクト | Symbol('x')(登録されていないもの) |
Date | プロトタイプが null のオブジェクト |
Map Set | |
BigInt | |
Promise | |
Server Component の要素(children) |
#JSON を基準にしない
いちばん多い誤解がこれです。JSON.stringify で通るかどうかは基準になりません。
Dateは JSON では文字列になるが、ここではそのまま渡るMapは JSON では空オブジェクトになるが、ここではそのまま渡る
基準は「実行できる形を持っているか」です。関数とクラスのインスタンスだけが、それを持っています。
// 渡る。受け取る側で toISOString() も size も使える
<View at={new Date()} counts={new Map([['a', 1]])} />
#Symbol には例外がある
Symbol は渡せませんが、Symbol.for で作ったものは渡せます。
<Tag mark={Symbol('label')} /> // ❌ ビルドが落ちる
<Tag mark={Symbol.for('label')} /> // ✅ 通る
Only global symbols received from Symbol.for(...) can be passed to Client Components.
Symbol('label') は、同じ文字列で作っても毎回別物になります。向こう側で同じものを
作り直せないので運べません。Symbol.for はグローバルなレジストリに登録するので、
キーさえ運べば向こう側で同じものを取り出せます。
境界の規則は「値の種類」ではなく、向こう側で復元できるかで並んでいます。
#例外:Server Function は渡せる
関数は渡せない、と書きましたが、Server Function だけは例外です。
// app/actions.ts
'use server'
export async function addToCart(id: number) {
// サーバー側で実行される
}
<AddButton onAdd={addToCart} /> // 渡せる
ただしこれは「関数が渡っている」のではありません。参照だけが渡り、呼ぶとサーバー側で実行されます。 中身のコードはブラウザに届きません。
#渡した props はすべてブラウザに届く
これが実務でいちばん重要な点です。
境界を越えた props は、画面に表示していなくてもブラウザに届きます。開発者ツールで読めます。
// 危険。ADMIN_KEY がブラウザに届く
return <Table rows={rows} apiKey={process.env.ADMIN_KEY} />
// 危険。row にパスワードのハッシュや内部フラグが含まれていれば、それも届く
const row = await db.user.findFirst()
return <Profile user={row} />
「使っていないから大丈夫」ではありません。渡した時点で届いています。
#実務での書き方
ORM が返した行をそのまま渡さず、必要な値だけを明示的に組み立てます。
const row = await db.user.findFirst()
const user = {
id: row.id,
name: row.name,
joinedAt: row.joinedAt, // Date はそのままでよい
}
return <Profile user={user} />
これは3つの問題を同時に解決します。
- クラスのインスタンスを渡してしまう事故を防ぐ
- 送りたくない列がブラウザに届く事故を防ぐ
- 送るデータ量が減る
#やってはいけない回避策
<Profile user={JSON.parse(JSON.stringify(row))} />
エラーは消えますが、
Dateが文字列に変わり、受け取る側で壊れるundefinedのキーが消える- 送りたくない列は依然として送られる
ので、直したことになりません。必要な値を選ぶほうが短く、安全です。
#迷ったときの判断
- その値は実行できる形を持っているか(関数・クラス)→ 渡せない
- サーバーで処理させたいのか → Server Function にする
- 受け取る側が本当に Client である必要があるか → 無ければ境界を引き直す
- その値がブラウザに届いて困らないか → 困るなら渡さない