Only plain objects の直し方
実際に出るエラー
クラスのインスタンスを Server Component から Client Component へ渡したときに出るエラーです。
#何が起きているか
Server Component から Client Component へ渡す props は、値だけがネットワークを越えます。
クラスのインスタンスは、値のほかにプロトタイプ(メソッドの置き場)を持っています。値だけを運ぶと、メソッドが消えた別物になってしまう。React はそういう「一部だけ壊れた値」を作らず、境界で止めます。
渡せる 渡せない
───────────────────────────── ─────────────────────────────
文字列・数値・真偽値 関数
配列・プレーンなオブジェクト クラスのインスタンス
Date・Map・Set Symbol
BigInt・TypedArray プロトタイプが null のオブジェクト
Server Component の要素
Date や Map は渡せます。**JSON にできるかどうかは基準ではありません。**基準は「実行できる形を持っているか」です。
#原因はほぼこの3つ
1. ORM が返したオブジェクトをそのまま渡している
いちばん多い形です。ORM によっては、行を単なるオブジェクトではなくモデルのインスタンスとして返します。
const user = await db.user.findFirst()
return <Profile user={user} /> // ← user がインスタンスだと止まる
2. 自作のクラスを渡している
Money、DateRange、Result のような値オブジェクトです。
3. ライブラリの戻り値を渡している
Decimal、Big.js、Temporal のポリフィル、独自の Error サブクラスなどが該当します。
#最短の確認手順
エラー文には、止まった props の中身が出ます。まずそれを読んでください。それでも分からないときは、渡す直前でこう確かめます。
console.log(Object.getPrototypeOf(value) === Object.prototype)
// false ならプレーンなオブジェクトではない
#直し方
推奨:必要な値だけを取り出して渡す
const row = await db.user.findFirst()
const user = {
id: row.id,
name: row.name,
joinedAt: row.joinedAt, // Date はそのままでよい
}
return <Profile user={user} />
これには副次的な効果があります。渡した props はすべてブラウザに届くので、必要な列だけに絞ることは、そのまま「余計なデータを送らない」ことになります。パスワードのハッシュや内部フラグを含む行を丸ごと渡してしまう事故も防げます。
境界を引き直す
そもそも Client Component である必要がないなら、'use client' を外すのがいちばん簡単です。境界を越えなければ、インスタンスのまま扱えます。
やってはいけない直し方:JSON で往復させる
<Profile user={JSON.parse(JSON.stringify(row))} />
エラーは消えますが、
Dateが文字列に変わる(受け取る側で壊れる)undefinedのキーが消える- 送りたくない列まで一緒に送ってしまう
ので、直したことになりません。必要な値を明示的に選ぶほうが、短く書けて安全です。
#なぜ開発中に気づきにくいのか
このエラーは描画のときに出ます。prerender される構成ならビルド中に失敗するので気づけますが、リクエストごとに描画される構成では、そのページを開くまで出ません。
さらに、データの中身によって出たり出なかったりします。findFirst() が null を返す経路しか通っていないと、開発中は一度も出ないことがあります。
#再発防止
- Server Component から Client Component へ渡す形を、明示的に組み立てると決めておく
- ORM の行をそのまま渡さない
- 渡す props の型を、プレーンな形で先に定義しておく
#関連するエラー
関数を渡した場合は文言が変わります。
Event handlers cannot be passed to Client Component props.
原因は同じ「境界を越えられないものを渡した」で、種類が違うだけです。