登録処理、削除処理実装のタイミングで同じエラーに引っかかったので備忘録として残しておきます。
要約
- Server Component から Client Component へ「通常の関数」は渡せない
-
解決策: Props で関数を渡すのをやめ、Client Component 側で
"use server"付きの関数(Server Action)を直接インポートする。
エラー内容
親(Server Component)で関数を作成し、Props 経由で子(Client Component)へ渡そうとしたら以下のエラーが発生した。
エラーメッセージ:
Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
// page.tsx (Server Component)
import TransactionDetail from "./components/TransactionDetail";
import { deleteTransaction } from "./actions/deleteTransaction";
export default async function Page({ params }) {
// 親側で関数をラップして渡そうとすると、境界線を越えられずエラーになる
const handleClickDelete = async () => {
await deleteTransaction(params.id);
};
return <TransactionDetail handleClickDelete="{handleClickDelete}"/>;
}
修正後
親からはデータ(IDなど)だけを渡し、Server Action("use server")は Client Component 側で直接インポートします。
1. Server Action(actions/deleteTransaction.ts)
先頭に "use server" を付与します。これがないとブラウザ側から呼び出せません。
"use server";
import { getRepository } from "@/utils/data-source";
import { Transaction } from "@/entities/Transaction";
export async function deleteTransaction(id: string) {
const repo = await getRepository(Transaction);
await repo.softDelete(id);
}
補足: "use server" を付け忘れるとどうなるか?
Client Component からインポートしている deleteTransaction.ts の "use server" を消す(つけ忘れる)と、以下のようなエラーが発生します。
Module not found: Can't resolve 'fs'
これは、Node.js 専用のモジュール(DB接続ライブラリ等)がブラウザ環境用コードとして読み込まれてしまうために起こります。
Client Component から呼び出すサーバー処理には、必ず "use server" が必須です。
2. 親コンポーネント(page.tsx / Server Component)
関数は渡さず、必要な ID のみを Props で渡します。
// page.tsx
import TransactionDetail from "./components/TransactionDetail";
export default async function Page({ params }) {
const { transactionId } = await params;
return <TransactionDetail transactionId="{transactionId}"/>;
}
3. 子コンポーネント(TransactionDetail.tsx / Client Component)
Server Action を直接インポートして実行します。
// TransactionDetail.tsx
"use client";
import { useRouter } from "next/navigation";
import { deleteTransaction } from "../actions/deleteTransaction";
export default function TransactionDetail({ transactionId }: { transactionId: string }) {
const router = useRouter();
const handleDelete = async () => {
try {
// Client Componentから直接Server Actionを呼び出す
await deleteTransaction(transactionId);
router.push("/dashboard");
} catch (e) {
alert("削除失敗");
}
};
return <button onClick={handleDelete}>削除</button>;
}
まとめ
Next.js (App Router) では、サーバーとクライアントの「境界線(Network Boundary)」を正しく意識することが重要です。
1. サーバーからクライアントへ関数を渡してはいけない
- NG: Server Component から Client Component へ、Props で関数を渡す。
- 理由: JavaScript の関数オブジェクトは境界線を越えてシリアライズ(データ化)できないため、Next.js で実行時エラーになります。
2. データだけを渡し、Server Action は子側で直接インポートする
-
OK: 親(Server Component)からは ID などの「値」だけを渡し、Client Component 側で
"use server"が付いた関数を直接importして呼び出す。 -
理由:
"use server"を宣言した関数であれば、Client Component から直接インポートしても Next.js が自動的に通信用 API を生成し、ブラウザとサーバー間を安全に橋渡ししてくれるためです。