RSC環境において、サーバーコンポーネントからクライアントコンポーネントにコールバックを渡したい
# 事象
Next.jsでサーバーコンポーネント(親)からクライアントコンポーネント(子)にコールバック関数をpropsとして渡したい、となった時に、そのまま実装すると下記のエラーで阻まれた。
code:text
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". Or maybe you meant to call this function rather than return it.
調べてみた結果などから判断するに、サーバーコンポーネントはシリアライズ可能である必要があるのに、コールバック関数はサーバー側でのレンダリング処理の後でも関数のまま残ってしまい、それはシリアライズ可能じゃないよね、ということらしい。
試してみた中で分かったのは、
サーバーコンポーネントからサーバーコンポーネントへはコールバック関数を渡せる(子側のサーバーレンダリング時に関数が実行されてシリアライズされた状態になるものであれば、の前提)
クライアントコンポーネントからクライアントコンポーネントへも渡せる(これは当たり前か...)
サーバーコンポーネントからクライアントコンポーネントへは渡せない(前述の通り)
クライアントコンポーネントからサーバーコンポーネントへ渡せるかは試せていない(が、これも子側のサーバーレンダリング時に関数が実行されるのであれば可能な気はする)
ということで、結局ダメなのは「サーバーコンポーネントからクライアントコンポーネントへのコールバック受け渡し」のパターンのみだと推察される。
# 回避策
ではどうするか、というところで、元々ダメだった
code:text
サーバーコンポーネント(親)
コールバック関数
↓
クライアントコンポーネント(子)
の状態を
code:text
サーバーコンポーネント(親)
↓
クライアントコンポーネントのWrapperになるクライアントコンポーネント(子)
コールバック関数
↓
クライアントコンポーネント(孫)
として、間に噛ませたWrapper(これもクライアントコンポーネント)が親の代わりにコールバック関数を渡す、で解決できる。
ただこれだと、コールバック渡しで既に若干複雑さが増しているところに、Wrapperを噛ませないといけないという制約がついてさらに複雑になるので、今回の案件では子をクライアントコンポーネントではなくサーバーコンポーネントに切り替える(子側の要件を変えた上で)という形に落ち着いた。
※ じゃあなんでわざわざ記事にしたんだと思われるかもしれないが、自分の備忘録として、Wrapperを噛ませるアイデアと諸々調査した結果を残しておきたかった、というのがこの記事の意図するところです。
# 余談
そもそもなぜコールバックを渡したいとなったか、について。
出発点としては、react-tableを使ったクライアントコンポーネントを汎用化して複数の親から共通して利用したい、というところ。
このテーブルの各行には、リンクの入ったセルがあるのだが、親側からこのリンクのURL(パラメータ付き)を動的に指定したいとなった時に、コールバックを利用する以外に方法が思いつかなかった。
すなわち、
code:page.tsx
"use server" // Next.js v15では記載は不要だが、ここではわかりやすくするためあえて明示
import { TableComponent } from "./child.tsx"
export const Page = async () => {
return (
<TableComponent
links={({ userId }) => [
{
text: "詳細を表示"
href: /user/${userId}/detail
},
{
text: "編集"
href: /user/${userId}/edit
}
]}
/>
)
}
code:child.tsx
"use client"
import { Link } from "next/link"
type Props = {
links: ({ userId }: { userId: string }) => {
text: string
href: string
}[]
}
export const TableComponent = ({ links }: Props) => {
// (省略)
return (
{/** 中略 **/}
<td>
{links(user.id).map((link) => (
<Link
key={link.text}
href={link.href}
{link.text}
</Link>
))}
</td>
{/** 後略 **/}
)
}
といったようなことをしたかったが、これではダメでした、という話。