Skip to content

イベントハンドラーと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では:

  1. すべてのコンポーネントはデフォルトでサーバーコンポーネント
  2. サーバーコンポーネントはブラウザで実行されない
  3. イベントハンドラーはクライアント(ブラウザ)サイドでのみ実行可能
  4. 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>
  );
}

この方法のメリット

  1. サーバーサイド処理とクライアントインタラクティブを分離可能
  2. 不要なJavaScriptバンドルサイズを削減
  3. サーバーコンポーネントで直接async/awaitが使用可能
  4. コードの関心を適切に分離

方法 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を使うことで実行をイベントハンドラー時に遅延
  • バインド後に関数を渡すことで安全に実行

ベストプラクティス

  1. 最小のクライアントコード原則

    • インタラクティブ要素だけクライアントコンポーネント化
    • 大きなコンポーネントの完全クライアント化は避ける
  2. コンポーネント設計方針

  3. サードパーティライブラリ使用時

    • UIライブラリ(例: SweetAlert2)はクライアントコンポーネント内でのみ使用
    • サーバーコンポーネント内でインポートするとエラー発生

パフォーマンス注意点

  • 不必要なクライアントコンポーネント化はバンドルサイズ増加の原因
  • 'use client'を含むファイルからインポートされたモジュールは自動的にクライアントバンドルに含まれる
  • ツリーシェイキングを意識したコンポーネント設計を

結論

Next.jsアプリケーションでイベントハンドラーを使用するには:

  1. 基本対応: コンポーネント最上部に'use client'追加でクライアント化
  2. 推奨方法: インタラクティブ部分だけを分離したクライアントコンポーネント作成
  3. データ送信: フォームのactionでサーバーアクションを使用
  4. 例外ケース: サーバーアクション渡しにはbindを活用

イベント関連のエラー解決には、各コンポーネントの実行環境(サーバー/クライアント)を明確に意識することが最も重要です。