Event handlers cannot be passed の直し方
実際に出るエラー
Server Component から Client Component へ関数を props で渡したときに出るエラーです。
#何が起きているか
App Router のコンポーネントは、既定で Server Component です。Server Component が返した内容は、そのままブラウザに届くわけではありません。いったん値だけの形に変換されてネットワークを越え、ブラウザ側で組み立て直されます。
関数はこの変換ができません。関数の中身は「実行できる形」であって、値ではないからです。だから境界で止められます。
Server Component ── 値だけが越える ──▶ Client Component
✅ 文字列・数値・配列・オブジェクト
✅ Date・Map・Set・BigInt
❌ 関数
❌ クラスのインスタンス
#原因はほぼこの3つ
1. onClick や onChange を Server Component から渡している
いちばん多い形です。
// app/cart/page.tsx(Server Component)
export default async function Page() {
const items = await db.item.findMany()
return <List items={items} onAdd={add} /> // ← ここ
}
2. コールバックを props 名で渡している
onXxx という名前でなくても同じです。formatter={fn} でも render={fn} でも、関数なら越えられません。
3. ライブラリのコンポーネントに関数を渡している
自分では境界を意識していなくても、そのライブラリが Client Component であれば同じことが起きます。
#最短の確認手順
- エラー文の
^^^^^^^^^^が指している props 名を見る - その props を渡している側のファイルを開く
- そのファイルの先頭に
'use client'があるか確認する - **無ければ、そこが Server Component です。**渡している値が関数なら、それが原因です
#直し方
推奨:ハンドラを Client Component の内側で定義する
境界を「ブラウザの操作が必要な部品」まで下ろします。
// app/cart/page.tsx(Server Component のまま)
export default async function Page() {
const items = await db.item.findMany()
return <List items={items} />
}
// components/List.tsx
'use client'
export function List({ items }: { items: Item[] }) {
const add = (id: number) => { /* ブラウザ側の処理 */ }
return items.map((i) => <button key={i.id} onClick={() => add(i.id)}>追加</button>)
}
Page は Server Component のままなので、DB に直接アクセスできますし、商品データを扱うコードはブラウザに送られません。
サーバー側で処理したい場合:Server Function を渡す
「押したらサーバーで何かする」なら、Server Function は props として渡せます。関数そのものではなく参照だけが渡り、呼ぶとサーバー側で実行されるためです。
// app/cart/actions.ts
'use server'
export async function addToCart(id: number) {
// サーバー側で実行される
}
やってはいけない直し方:Page に use client を付ける
エラーは消えます。境界を越えなくなるからです。ですが、
- 商品データを取る処理まで含めてブラウザ側に置かれる
- DB への直接アクセスが成立しなくなる
- ページ全体の JavaScript がブラウザへ送られる
ので、解決にはなっていません。エラーが消えることと、正しく直ったことは違います。
#なぜ開発中に気づきにくいのか
このエラーは描画のときに出ます。そのページが静的に prerender される構成なら、その描画はビルド中に走るので、ビルドが失敗して気づけます。
一方、リクエストごとに描画される構成だと、そのページを実際に開くまで出ません。開発中に触っていない画面だと、本番で初めて表面化することがあります。
#再発防止
- 境界は葉に近い側へ引く。ページ全体に
use clientを付けない - Server Component から渡す props は「値」だけ、と決めておく
- ライブラリのコンポーネントを使うときは、それが Client Component かどうかを先に確認する
#関連するエラー
クラスのインスタンスを渡した場合は、別の文言になります。
Only plain objects, and a few built-ins, can be passed to Client Components
from Server Components. Classes or null prototypes are not supported.
原因は同じ「境界を越えられないものを渡した」で、越えられないものの種類が違うだけです。