イベントハンドラーとNext.jsクライアントコンポーネント
問題点
Next.js 13以降でボタンのonClick
イベントハンドラーを実装しようとすると、次のエラーが発生する場合があります:
Error: Event handlers cannot be passed to Client Component props.
^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
エラー原因
このエラーは、サーバーコンポーネントで直接イベントハンドラーを使用しようとした場合に発生します
tsx
// エラーが発生するサーバーコンポーネントの例
const reqHelp = () => {
// Swal.fire(...) // SweetAlert2などのUIライブラリ呼び出し
}
return (
<div className="buttons">
<button onClick={reqHelp}>Request Help</button> {/* ここでエラー発生 */}
</div>
);
根本原因
Next.js 13以降のApp Routerでは:
- すべてのコンポーネントはデフォルトでサーバーコンポーネント
- サーバーコンポーネントはブラウザで実行されない
- イベントハンドラーはクライアント(ブラウザ)サイドでのみ実行可能
onClick
のようなイベントハンドラーはクライアントコンポーネントでのみ使用可能
Next.jsのレンダリングモデル
コンポーネント種別 | 実行環境 | イベントハンドラー | データ取得 |
---|---|---|---|
サーバーコンポーネント | サーバー | × | ○ (async/await可) |
クライアントコンポーネント | ブラウザ | ○ | × |
解決方法
方法 1: コンポーネント全体をクライアントコンポーネントに変換
ファイルの先頭に'use client'
ディレクティブを追加:
tsx
'use client'; // これが必須(最上部に配置)
const MyComponent = () => {
const reqHelp = () => {
Swal.fire({ /* ... */ }); // クライアントサイドで実行
}
return (
<div className="buttons">
<button onClick={reqHelp}>Request Help</button>
</div>
);
}
ディレクティブの配置ルール
- 必ずファイルの一番最初の行に記述
- コメントや空行より前に記述すること
- インポート文よりも前に配置 ❌
import ...
→'use client'
(不可) ✅'use client'
→import ...
(正しい)
方法 2: インタラクティブ部分だけをクライアントコンポーネントとして分離(推奨)
コンポジションパターンでサーバー/クライアントコンポーネントを組み合わせる:
tsx
'use client'; // クライアントコンポーネント
export const InteractiveButton = () => {
const handleClick = () => {
// クライアントサイドロジック
};
return <button onClick={handleClick}>クリック</button>;
}
tsx
import { InteractiveButton } from './Button.client'; // クライアントコンポーネントをインポート
export default async function Page() {
// サーバーサイドデータ取得
const data = await fetchData();
return (
<div>
<!-- サーバーコンポーネント部分 -->
<h1>{data.title}</h1>
<!-- クライアントコンポーネント使用 -->
<InteractiveButton />
</div>
);
}
この方法のメリット
- サーバーサイド処理とクライアントインタラクティブを分離可能
- 不要なJavaScriptバンドルサイズを削減
- サーバーコンポーネントで直接
async/await
が使用可能 - コードの関心を適切に分離
方法 3: フォームアクションでサーバーサイド処理(Next.js 13.4+)
onSubmit
ではなくフォームのaction
プロパティを使用:
tsx
'use server'; // サーバーアクション定義ファイルで使用
export async function sendData(formData: FormData) {
// サーバーサイドで実行される処理
const email = formData.get('email');
await saveToDatabase(email);
}
tsx
import { sendData } from './actions'; // サーバーアクションをインポート
const ContactForm = () => (
<form action={sendData}>
<input name="email" />
<button type="submit">送信</button>
</form>
);
特殊ケース: サーバーアクションをプロパティとして渡す
クライアントコンポーネントにサーバーアクションを渡す場合はbind
を使用:
tsx
'use server';
export async function serverAction(id: string) {
// データ処理
}
tsx
import { serverAction } from './actions';
const ClientComponent = ({ itemId }) => {
// 引数の事前バインド
const boundAction = serverAction.bind(null, itemId);
return <button onClick={boundAction}>実行</button>;
}
bind
の重要性
- 直接
onClick={serverAction(itemId)}
とすると、レンダリング時に実行されてしまう bind
を使うことで実行をイベントハンドラー時に遅延- バインド後に関数を渡すことで安全に実行
ベストプラクティス
最小のクライアントコード原則
- インタラクティブ要素だけクライアントコンポーネント化
- 大きなコンポーネントの完全クライアント化は避ける
コンポーネント設計方針
サードパーティライブラリ使用時
- UIライブラリ(例: SweetAlert2)はクライアントコンポーネント内でのみ使用
- サーバーコンポーネント内でインポートするとエラー発生
パフォーマンス注意点
- 不必要なクライアントコンポーネント化はバンドルサイズ増加の原因
'use client'
を含むファイルからインポートされたモジュールは自動的にクライアントバンドルに含まれる- ツリーシェイキングを意識したコンポーネント設計を
結論
Next.jsアプリケーションでイベントハンドラーを使用するには:
- 基本対応: コンポーネント最上部に
'use client'
追加でクライアント化 - 推奨方法: インタラクティブ部分だけを分離したクライアントコンポーネント作成
- データ送信: フォームの
action
でサーバーアクションを使用 - 例外ケース: サーバーアクション渡しには
bind
を活用
イベント関連のエラー解決には、各コンポーネントの実行環境(サーバー/クライアント)を明確に意識することが最も重要です。