Reactのイベント処理

2025-08-08

はじめに

この記事では、Reactにおけるイベント処理について、初学者向けにていねいに解説します。

イベント処理とは「ボタンをクリックしたとき」「テキストを入力したとき」「マウスを重ねたとき」など、ユーザーの操作に対して何かを実行する仕組みです。Reactでのイベント処理は、通常のHTMLと似た書き方ですが、いくつか重要な違いがあります。この記事を読み終えると、クリック・キーボード・マウスイベントを自在に扱えるようになります。

HTMLとReactのイベント処理:何が違うの?

まず、通常のHTMLとReactでの書き方の違いを見てみましょう。

/* ---- HTML の書き方 ---- */
<button onclick="handleClick()">クリック</button>
{/* イベント名が小文字、文字列で関数名を渡す */}

/* ---- React の書き方 ---- */
<button onClick={handleClick}>クリック</button>
{/* イベント名がキャメルケース、関数そのものを{}で渡す */}

違いは3点あります。

  • イベント名がキャメルケースonclickonClickonmouseenteronMouseEnter
  • 関数そのものを渡す:文字列ではなく {handleClick} のように波括弧で関数を渡す
  • デフォルト動作の防止方法が違う:HTMLの return false は効かず、event.preventDefault() を明示的に呼ぶ必要がある

デフォルト動作の防止方法が違う」とは、HTMLや通常のJavaScriptでは、イベント処理の最後で return false を書くと、リンク移動やフォーム送信などのデフォルト動作を止められる場合があります。

しかしReactでは、return false を書いてもデフォルト動作は停止されません。Reactではイベントを独自の仕組みで管理しているため、動作を止めたい場合は、event.preventDefault()を明示的に呼び出す必要があります。

基本的なクリックイベント

まずは最もよく使う onClick から始めましょう。イベントハンドラー(イベントに反応する関数)を定義して、onClick に渡すだけです。

function ClickExample() {
  // ① イベントハンドラー関数を定義
  const handleClick = () => {
    console.log('ボタンがクリックされました!');
  };

  // ② onClick に関数を渡す(() をつけないこと!)
  return <button onClick={handleClick}>クリックしてください</button>;
}

ここで重要なのが onClick={handleClick} と書く点です。onClick={handleClick()}() をつけてしまうと、ページが表示された瞬間に関数が実行されてしまいます。

// ❌ 誤り:ページ表示と同時に実行される(クリックを待たない)
<button onClick={handleClick()}>クリック</button>

// ✅ 正しい:クリックされたときだけ実行される
<button onClick={handleClick}>クリック</button>

イベントハンドラーの3つの書き方

イベントハンドラーを定義する方法は主に3通りあります。状況に応じて使い分けましょう。

書き方1:関数宣言で定義する

Reactでは、以下のようにイベント時に実行したい関数を onClick に指定してイベント処理を書きます。

function ButtonComponent() {
  function handleClick() {
    console.log('クリックされました');
  }

  return <button onClick={handleClick}>ボタン</button>;
}

書き方2:アロー関数で定義する(最もよく使う)

書き方1と同様に onClick に指定してイベント処理します。違いはアロー関数で定義していることです。

function ButtonComponent() {
  const handleClick = () => {
    console.log('クリックされました');
  };

  return <button onClick={handleClick}>ボタン</button>;
}

書き方3:インラインで直接書く(シンプルな1行処理向け)

function ButtonComponent() {
  return (
    <button onClick={() => console.log('クリックされました')}>
      ボタン
    </button>
  );
}

このコードは、クリックされたときに実行する処理を、その場に直接書いている形です。関数定義を別にせずに、処理が1〜2行程度ならインラインで、それ以上になるなら関数として切り出すのが読みやすくなります。

イベントオブジェクト(event)を使う

イベントハンドラーには、クリックやキー入力に関する詳細情報を持つ「イベントオブジェクト」が自動的に渡されます。引数として受け取ることができます。

function EventObjectExample() {
  const handleClick = (event) => {
    // event には様々な情報が入っている
    console.log('イベントの種類:', event.type);       // "click"
    console.log('クリックしたX座標:', event.clientX); // 例: 245
    console.log('クリックしたY座標:', event.clientY); // 例: 108
    console.log('クリックした要素:', event.target);   // <button>要素
  };

  return <button onClick={handleClick}>位置を確認</button>;
}

Reactのイベントオブジェクトは「合成イベント(SyntheticEvent)」と呼ばれる独自のオブジェクトですが、ブラウザのネイティブイベントオブジェクトと同じプロパティを持っているので、普通のDOMのイベントと同じ感覚で使えます。

よく使うイベントの種類

React で頻繁に使うイベントをまとめます。

イベント名発生するタイミングよく使う場面
onClickクリックしたときボタン、リンク
onChange値が変わったときテキスト入力、セレクトボックス
onSubmitフォームが送信されたときフォーム
onFocusフォーカスが当たったとき入力欄のハイライト
onBlurフォーカスが外れたとき入力欄のバリデーション
onMouseEnterマウスが要素に入ったときホバー効果
onMouseLeaveマウスが要素から出たときホバー効果
onKeyDownキーを押したときEnterキーで送信など
onKeyUpキーを離したときキーボードショートカット

マウスイベントの例

このコードは、マウスが要素に入った時と出た時のイベントを処理するReactコンポーネントです。onMouseEnter はマウスが <div> に入った時に実行され、コンソールに「マウスが入ってきた」と表示されます。onMouseLeave はマウスが <div> から出た時に実行され、「マウスが出ていった」と表示されます。つまり、「マウス操作に反応するイベント処理」の例です。

function HoverBox() {
  const handleMouseEnter = () => console.log('マウスが入ってきた');
  const handleMouseLeave = () => console.log('マウスが出ていった');

  return (
    <div
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      style={{ padding: '20px', backgroundColor: 'lightblue' }}
    >
      マウスをここに重ねてみてください
    </div>
  );
}

キーボードイベントの例

function EnterSearch() {
  const handleKeyDown = (event) => {
    // event.key で押されたキーの名前がわかる
    if (event.key === 'Enter') {
      console.log('Enterが押されました!検索を実行します');
    }
  };

  return (
    <input
      type="text"
      onKeyDown={handleKeyDown}
      placeholder="入力してEnterを押してください"
    />
  );
}

このコードは、入力欄でキーが押された時のイベントを処理しています。onKeyDown={handleKeyDown} によって、キーボード入力時に handleKeyDown 関数が実行されます。

その中で、「javascript id=”m551qa” event.key === ‘Enter’」を使い、「押されたキーが Enter かどうか」を判定しています。そのため、入力欄で Enter キーを押すと、「text id=”r882pd” Enterが押されました!検索を実行します」がコンソールに表示されます。つまり、「Enterキーが押されたら特定の処理を実行する」コードです。

イベントとStateの連携

イベント処理の最も重要な使い方は、イベントが起きたときに State を更新して画面を変化させることです。前の記事で学んだ useState と組み合わせることで、インタラクティブなUIが作れます。

クリックカウンター

このコードは、ボタンを押すことで数字を増減できるカウンターコンポーネントです。const [count, setCount] = useState(0);で count というStateを作成し、初期値を 0 にしています。increment は、setCount(count + 1)を実行して、カウントを1増やします。

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  // イベントハンドラーの中で State を更新する
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={increment}>増やす</button>
      <button onClick={decrement}>減らす</button>
    </div>
  );
}

decrement は、setCount(count – 1)を実行して、カウントを1減らします。そして、onClick={increment}やonClick={decrement}によって、ボタンがクリックされた時にStateが更新されます。Stateが変わると画面も自動で更新されるため、表示される数字がリアルタイムで変化します。

ボタンごとに異なるメッセージを表示

このコードは、ボタンを押すと表示メッセージが切り替わるReactコンポーネントです。「const [message, setMessage] = useState(‘ボタンをクリックしてください’);」で message というStateを作成し、最初は「ボタンをクリックしてください」が表示されます。

import { useState } from 'react';

function MessageSwitcher() {
  const [message, setMessage] = useState('ボタンをクリックしてください');

  return (
    <div>
      <p>{message}</p>
      <button onClick={() => setMessage('こんにちは!')}>挨拶</button>
      <button onClick={() => setMessage('さようなら!')}>別れ</button>
      <button onClick={() => setMessage('ボタンをクリックしてください')}>リセット</button>
    </div>
  );
}

各ボタンの onClick では setMessage() を使ってStateを変更しています。たとえば、setMessage(‘こんにちは!’)が実行されると、画面の表示が「こんにちは!」に変わります。つまり、「ボタン操作によってStateを変更し、画面表示を切り替える」コードです。

引数を渡すイベントハンドラー

リストの各アイテムのボタンなど、「どのアイテムがクリックされたか」を知りたい場合は、ハンドラーに引数を渡します。このときはインラインのアロー関数で包んで渡します。

function ItemList() {
  const items = ['りんご', 'バナナ', 'みかん'];

  // item を引数として受け取るハンドラー
  const handleItemClick = (itemName) => {
    console.log(`${itemName} がクリックされました`);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item}>
          {/* アロー関数で包むことで item を引数として渡せる */}
          <button onClick={() => handleItemClick(item)}>
            {item}
          </button>
        </li>
      ))}
    </ul>
  );
}

このコードは、配列に入っている商品名を一覧表示し、それぞれのボタンをクリックすると、その商品名をコンソールに表示するReactコンポーネントです。まず、りんご・バナナ・みかんという3つのデータを配列として用意しています。

そして map を使って配列の中身を1つずつ取り出し、ボタンの一覧を作成しています。ボタンがクリックされると、クリックされた商品の名前を handleItemClick に渡し、「○○ がクリックされました」とコンソールに表示します。

onClick の中でアロー関数を使っている理由は、クリックされた時に初めて関数を実行し、さらに「どの商品が押されたか」という値を引数として渡すためです。つまりこのコードは、「一覧データからボタンを動的に作り、それぞれのボタンで異なるデータを扱う方法」を表しています。

const handleItemClick = (itemName, event) => {
  console.log(`${itemName} がクリックされました`, event.type);
};

// 呼び出し側:e を受け取って両方渡す
<button onClick={(e) => handleItemClick(item, e)}>{item}</button>

イベントオブジェクト(event)と独自の引数を両方使いたいときは、次のように書きます。ボタンがクリックされると、まず onClick の中のアロー関数が実行されます。その時、Reactが自動で渡してくれるイベント情報を e として受け取り、それを handleItemClick に渡しています。

同時に、item も一緒に渡しているため、関数の中では「どの商品がクリックされたか」と「どんなイベントが発生したか」の両方を利用できます。event.type はイベントの種類を表しており、この場合はクリックイベントなので "click" が表示されます。

イベントの伝搬(バブリング)を理解しよう

HTMLのDOMでは、子要素のイベントが親要素にも伝わっていきます(これを「バブリング」と言います)。Reactでも同じ仕組みが適用されます。

Reactのバブリングとは、子要素で発生したイベントが親要素へ順番に伝わっていく仕組みのことです。たとえば、ボタンが <div> の中にある場合、ボタンをクリックすると、「ボタンのクリック処理」→「親の <div> のクリック処理」というようにイベントが上へ伝わります。これをイベントバブリングと呼びます。

以下のようにイベントバブリングが親へ伝わるのを止めたい場合は、stopPropagation() を使います。

function BubblingExample() {
  const handleParentClick = () => {
    console.log('② 親要素のクリックが発生');
  };

  const handleChildClick = (e) => {
    console.log('① 子要素のクリックが発生');
    // e.stopPropagation() を呼ばないと、親にも伝わる
  };

  return (
    <div
      onClick={handleParentClick}
      style={{ padding: '20px', backgroundColor: 'lightgray' }}
    >
      親要素
      <button onClick={handleChildClick}>子ボタン</button>
    </div>
  );
}
// 子ボタンをクリックすると、①→② の順に両方が実行される

子のイベントを親に伝えたくない場合は e.stopPropagation() を使います。

const handleChildClick = (e) => {
  e.stopPropagation(); // ← これで親への伝搬を止める
  console.log('子要素のクリックだけ処理');
};

フォームの送信などブラウザのデフォルト動作を止めたい場合は e.preventDefault() を使います。この場合は「子要素のクリック処理だけを実行し、親要素には影響させない」ためのものです。

const handleSubmit = (e) => {
  e.preventDefault(); // ← ページリロードを防ぐ
  console.log('フォームを送信しました(ページはリロードされない)');
};

カスタムコンポーネントにイベントを渡す

自分で作ったコンポーネントにもイベントハンドラーを Props として渡せます。onClick という名前で渡すのが一般的です。

// 汎用ボタンコンポーネント:onClick を Props で受け取る
function CustomButton({ onClick, children }) {
  return (
    <button
      onClick={onClick}
      style={{
        padding: '10px 15px',
        backgroundColor: '#4CAF50',
        color: 'white',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
      }}
    >
      {children}
    </button>
  );
}

function App() {
  const handleClick = () => {
    alert('カスタムボタンがクリックされました!');
  };

  return (
    <CustomButton onClick={handleClick}>
      クリックしてください
    </CustomButton>
  );
}

コンポーネント内でさらに処理を追加してから、Props の関数を呼ぶ「拡張パターン」も便利です。

function LoggingButton({ onClick, children }) {
  const handleClick = (event) => {
    console.log('ボタンがクリックされた(ログ記録)');
    if (onClick) {
      onClick(event); // Props の onClick も呼ぶ
    }
  };

  return <button onClick={handleClick}>{children}</button>;
}

実践的なイベント処理の例

ツールチップの実装

onMouseEnteronMouseLeave を使って、マウスを重ねると説明文が現れるツールチップを作れます。

import { useState } from 'react';

function Tooltip({ text, children }) {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div style={{ position: 'relative', display: 'inline-block' }}>
      <span
        onMouseEnter={() => setIsVisible(true)}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </span>
      {isVisible && (
        <div style={{
          position: 'absolute',
          bottom: '100%',
          left: '50%',
          transform: 'translateX(-50%)',
          backgroundColor: 'black',
          color: 'white',
          padding: '5px 10px',
          borderRadius: '4px',
          whiteSpace: 'nowrap',
        }}>
          {text}
        </div>
      )}
    </div>
  );
}

// 使い方
<Tooltip text="これはツールチップです">
  <button>ここにホバーしてください</button>
</Tooltip>

二重送信防止ボタン

ボタンをクリックした後、一定時間無効にすることで連打による二重送信を防げます。

import { useState } from 'react';

function SafeSubmitButton({ onClick, children }) {
  const [isDisabled, setIsDisabled] = useState(false);

  const handleClick = (event) => {
    if (isDisabled) return; // 処理中は何もしない

    setIsDisabled(true);  // ① ボタンを無効化
    onClick(event);       // ② 本来の処理を実行

    // ③ 1秒後に再び有効化
    setTimeout(() => setIsDisabled(false), 1000);
  };

  return (
    <button onClick={handleClick} disabled={isDisabled}>
      {isDisabled ? '処理中...' : children}
    </button>
  );
}

// 使い方
<SafeSubmitButton onClick={() => console.log('送信!')}>
  送信する
</SafeSubmitButton>

よくあるエラーと解決策

エラー・症状原因解決策
ページ表示と同時に関数が実行されるonClick={handleClick()} と書いているonClick={handleClick} に修正する
eventundefinedonClick={() => handleClick(event)} と書いているonClick={(e) => handleClick(e)} に修正する
クラス型で thisundefinedメソッドに this がバインドされていないアロー関数で定義するか、コンストラクタで bind する
子のクリックが親にも伝わるイベントバブリングが発生している子ハンドラーで e.stopPropagation() を呼ぶ
フォームを送信するとページがリロードされるブラウザのデフォルト動作が実行されているe.preventDefault() を呼ぶ

まとめ

Reactのイベント処理の重要なポイントを整理しておきましょう。

イベント名はキャメルケースで記述し、onClick・onChange・onKeyDown などの形式を用います。イベントハンドラーには関数そのものを渡し、onClick={handleClick} のように記述し、handleClick() のように実行はしません。イベントオブジェクトはハンドラーの引数として受け取ることができ、event.type や event.target などの情報にアクセスできます。また、Stateとの連携として、ハンドラー内で setState を呼び出すことで画面の更新が可能です。引数を渡したい場合は、onClick={() => handle(item)} のようにアロー関数で包みます。さらに、イベントの伝搬は e.stopPropagation() を使うことでバブリングを停止できます。

演習問題

🟢 初級問題(問題1〜3)

イベントハンドラーの基本的な書き方・イベントオブジェクトの使い方・Stateとの基本的な組み合わせを練習します。

問題1:クリックイベントの基本を実装しよう

【問題】
以下の条件をすべて満たす AlertButton コンポーネントを作成してください。

  • ボタンに「メッセージを表示」と表示する
  • クリックすると alert("こんにちは!Reactのイベント処理です。") を実行する
  • ハンドラー関数を handleClick という名前で コンポーネント内の変数として定義し、onClick に渡すこと(インラインに直接書かないこと)

【期待する動作】

ボタンをクリックするとアラートが表示される。

問題2:イベントオブジェクトの情報を表示しよう

【問題】
以下の条件をすべて満たす ClickInfo コンポーネントを作成してください。

  • State として info(文字列、初期値: "ボタンをクリックしてください")を管理する
  • ボタンをクリックしたとき、イベントオブジェクトの clientXclientY を使って "クリック座標: X=〇〇, Y=〇〇" という文字列を info に設定する
  • info<p> タグで画面に表示する

【期待する表示(クリック後)】

クリック座標: X=320, Y=214
[ここをクリック]

問題3:キーボードイベントでEnter送信を実装しよう

【問題】
以下の条件をすべて満たす EnterInput コンポーネントを作成してください。

  • State として inputValue(文字列、初期値: "")と submitted(文字列、初期値: "")を管理する
  • inputonChangeinputValue を更新する
  • onKeyDownevent.key === "Enter" のとき、inputValue の内容を submitted に設定し、inputValue を空にする
  • submitted が空でなければ "送信された内容: 〇〇" と表示する

【期待する動作(”React” と入力してEnter後)】

[                    ]  ← 入力欄は空になる
送信された内容: React

🟡 中級問題(問題4〜9)

引数付きイベント・バブリング制御・カスタムコンポーネントへのイベント渡し・複合的なState連携を練習します。

問題4:リストアイテムに引数付きクリックを実装しよう

【問題】
以下の条件をすべて満たす ColorPalette コンポーネントを作成してください。

const colors = [
  { id: 1, name: "赤",   hex: "#e74c3c" },
  { id: 2, name: "青",   hex: "#3498db" },
  { id: 3, name: "緑",   hex: "#2ecc71" },
  { id: 4, name: "黄",   hex: "#f1c40f" },
];
  • State として selected(初期値: null)を管理する
  • 各色を <button> で表示し、ボタンの背景色を hex で設定する
  • ボタンをクリックすると、そのカラーオブジェクトを selected にセットする(引数付きアロー関数で渡すこと)
  • selectednull でなければ "選択中: 赤(#e74c3c)" のように表示する

【期待する表示(「青」クリック後)】

[赤] [青] [緑] [黄]   ← 各ボタンはそれぞれの色
選択中: 青(#3498db)

問題5:バブリングをstopPropagationで制御しよう

【問題】
以下の条件をすべて満たす CardWithButton コンポーネントを作成してください。

  • State として log(文字列配列、初期値: [])を管理する
  • 外側の <div>(カード全体)をクリックすると "カード全体がクリックされました"log に追加する
  • カード内部の「詳細を見る」ボタンをクリックすると "詳細ボタンがクリックされました"log に追加するが、カード全体のクリックは発火させないe.stopPropagation() を使うこと)
  • log の内容を <ul> で一覧表示する

【期待する動作(詳細ボタン→カード全体の順でクリック)】

• 詳細ボタンがクリックされました   ← カードには伝播しない
• カード全体がクリックされました

問題6:フォームのデフォルト動作をpreventDefaultで防ごう

【問題】
以下の条件をすべて満たす SearchForm コンポーネントを作成してください。

  • State として query(文字列、初期値: "")と result(文字列、初期値: "")を管理する
  • <form> タグの onSubmite.preventDefault() を呼び、ページリロードを防ぐ
  • 送信時に result"「〇〇」を検索しました"(〇〇は query の内容)に更新し、query を空にする
  • result が空でなければ結果を表示する

【期待する動作(”React” で検索後)】

[        ] [検索]
「React」を検索しました

問題7:カスタムボタンコンポーネントにイベントを渡そう

【問題】
以下の条件をすべて満たすプログラムを作成してください。

  • IconButton コンポーネントを作成する。Props として onClickicon(絵文字)・label(テキスト)を受け取り、"[icon] label" 形式のボタンを表示する
  • 親コンポーネント App で State として count(数値、初期値: 0)を管理する
  • IconButton を3つ使い、それぞれ「➕ 増やす」「➖ 減らす」「🔄 リセット」ボタンを実装する
  • 現在の countApp の中で表示する

【期待する表示(初期状態)】

カウント: 0
[➕ 増やす] [➖ 減らす] [🔄 リセット]

問題8:ホバーで背景色が変わるカードを作ろう

【問題】
以下の条件をすべて満たす HoverCard コンポーネントを作成してください。

const cards = [
  { id: 1, title: "Reactの基礎",    desc: "JSXとコンポーネントを学ぶ" },
  { id: 2, title: "PropsとState",   desc: "データの管理方法を理解する" },
  { id: 3, title: "イベント処理",   desc: "インタラクションを実装する" },
];
  • State として hoveredId(数値 or null、初期値: null)を管理する
  • 各カードの onMouseEnter でそのカードの idhoveredId に設定し、onMouseLeavenull に戻す
  • ホバーされているカードの背景色を "#e8f4fd"、それ以外は "#fff" にする(インラインスタイルで切り替えること)

問題9:onFocusとonBlurでフォーカス状態を管理しよう

【問題】
以下の条件をすべて満たす FocusInput コンポーネントを作成してください。

  • State として isFocused(真偽値、初期値: false)と value(文字列、初期値: "")を管理する
  • onFocusisFocusedtrue に、onBlurfalse に更新する
  • フォーカス中は入力欄の枠線を "2px solid #3498db"、非フォーカス時は "1px solid #ccc" にする(style 属性で切り替えること)
  • フォーカス中は入力欄の上に "✏️ 入力中..." と表示し、フォーカスが外れたら "入力値: 〇〇"(空のときは非表示)を表示する

【期待する表示(フォーカス中・”Hello” 入力)】

✏️ 入力中...
[Hello               ]  ← 青い枠線

🔴 上級問題(問題10〜12)

複数のイベントを組み合わせた複合的な実装・カスタムコンポーネントの拡張・実践的なUIコンポーネントの設計に挑戦します。

問題10:キーボードショートカット付きカウンターを実装しよう

【問題】
以下の条件をすべて満たす KeyboardCounter コンポーネントを作成してください。

  • State として count(数値、初期値: 0)を管理する
  • 画面上にボタン(「+1」「−1」「リセット」)を表示し、クリックで操作できる
  • さらに キーボードでも操作できるようにするdiv 要素に onKeyDown を付け、以下のショートカットを実装する
キー動作
ArrowUp+1
ArrowDown−1
rリセット
  • キーボードで操作できるよう、divtabIndex={0} を付けること(これでフォーカスを当てられるようになる)
  • 使えるショートカットの説明を画面に表示すること

問題11:ドラッグ&ドロップ風の並び替えリストを実装しよう

【問題】
マウスイベントを使って、クリックで「選択」→別の位置に「移動」できるリストを実装してください。

const initialItems = ["React", "JavaScript", "TypeScript", "Node.js", "CSS"];
  • State として items(配列)と selectedIndex(数値 or null、初期値: null)を管理する
  • アイテムをクリックすると selectedIndex にそのインデックスを設定し、選択中アイテムの背景色を変える
  • selectedIndex が設定済みの状態で別のアイテムをクリックすると、選択中のアイテムをそのアイテムの前に挿入して並び替える(filter()splice() または スプレッド構文で新しい配列を作ること)
  • 並び替え後、selectedIndexnull にリセットする

【期待する動作】

1回目クリック: "TypeScript" を選択(ハイライト)
2回目クリック: "React" をクリック → TypeScript が React の前に移動

結果:
• TypeScript  ← 移動された
• React
• JavaScript
• Node.js
• CSS

問題12:アコーディオンUIを実装しよう

【問題】
クリックで開閉するアコーディオン(折りたたみ)コンポーネントを実装してください。

const faqs = [
  { id: 1, question: "Reactとは何ですか?",      answer: "UIを構築するためのJavaScriptライブラリです。" },
  { id: 2, question: "なぜReactを使うのですか?", answer: "コンポーネントベースの設計で再利用性が高く、効率的な開発が可能です。" },
  { id: 3, question: "Hooksとは何ですか?",       answer: "React 16.8で導入された、関数型コンポーネントで状態管理などを行う仕組みです。" },
];
  • AccordionItem コンポーネントquestionanswerisOpenonToggle を Props で受け取る。question をクリックすると onToggle を呼ぶ。isOpentrue のときだけ answer を表示する
  • Accordion コンポーネントopenId(数値 or null、初期値: null)を State として管理する。同じ質問を再度クリックすると閉じる(openId === id ? null : id)ように切り替える。faqsmap()AccordionItem として展開する
  • 一度に開いているのは常に1項目のみ(別の質問を開くと前の質問は閉じる)

【期待する表示(2番目が開いている状態)】

▶ Reactとは何ですか?
▼ なぜReactを使うのですか?
    コンポーネントベースの設計で再利用性が高く、効率的な開発が可能です。
▶ Hooksとは何ですか?

📘 解説(全12問)

解説1:クリックイベントの基本を実装しよう

【解答例】

function AlertButton() {
  // コンポーネント内に関数を定義
  const handleClick = () => {
    alert("こんにちは!Reactのイベント処理です。");
  };

  // onClick に関数を渡す(()は不要)
  return <button onClick={handleClick}>メッセージを表示</button>;
}

【解説】

  • handleClick をコンポーネント内の変数として定義することで、処理の内容がひと目でわかる
  • onClick={handleClick} と書く。handleClick() と書くと即時実行されてしまうため注意
  • 処理が1行なら onClick={() => alert("...")} のようにインラインで書くのも有効だが、複数行になるなら関数として切り出す

解説2:イベントオブジェクトの情報を表示しよう

【解答例】

import { useState } from 'react';

function ClickInfo() {
  const [info, setInfo] = useState("ボタンをクリックしてください");

  const handleClick = (event) => {
    // event.clientX / clientY でクリック座標を取得
    setInfo(`クリック座標: X=${event.clientX}, Y=${event.clientY}`);
  };

  return (
    <div>
      <p>{info}</p>
      <button onClick={handleClick}>ここをクリック</button>
    </div>
  );
}

【解説】

  • イベントハンドラーの第1引数に event(慣例として e とも書く)を受け取ると、クリック情報にアクセスできる
  • event.clientX / clientY はブラウザのウィンドウ左上からのピクセル座標
  • テンプレートリテラル(`文字列${変数}`)を使って座標を文字列に埋め込む

解説3:キーボードイベントでEnter送信を実装しよう

【解答例】

import { useState } from 'react';

function EnterInput() {
  const [inputValue, setInputValue] = useState("");
  const [submitted,  setSubmitted]  = useState("");

  const handleKeyDown = (event) => {
    if (event.key === "Enter" && inputValue.trim() !== "") {
      setSubmitted(inputValue.trim());
      setInputValue(""); // 入力欄をリセット
    }
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyDown={handleKeyDown}
        placeholder="入力してEnterを押してください"
      />
      {submitted && <p>送信された内容: {submitted}</p>}
    </div>
  );
}

【解説】

  • event.key でどのキーが押されたかの名前が取得できる。Enterは "Enter"、矢印キーは "ArrowUp" など
  • onChange で入力値をリアルタイムに State に反映し、onKeyDown で確定処理を行うのが基本パターン
  • inputValue.trim() !== "" で空文字を除外することで、空のままEnterを押しても送信されない

解説4:リストアイテムに引数付きクリックを実装しよう

【解答例】

import { useState } from 'react';

const colors = [
  { id: 1, name: "赤", hex: "#e74c3c" },
  { id: 2, name: "青", hex: "#3498db" },
  { id: 3, name: "緑", hex: "#2ecc71" },
  { id: 4, name: "黄", hex: "#f1c40f" },
];

function ColorPalette() {
  const [selected, setSelected] = useState(null);

  return (
    <div>
      <div>
        {colors.map((color) => (
          <button
            key={color.id}
            onClick={() => setSelected(color)} // アロー関数で color を引数として渡す
            style={{
              backgroundColor: color.hex,
              color: 'white',
              margin: '4px',
              padding: '8px 16px',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer',
            }}
          >
            {color.name}
          </button>
        ))}
      </div>
      {selected && (
        <p>選択中: {selected.name}({selected.hex})</p>
      )}
    </div>
  );
}

【解説】

  • onClick={() => setSelected(color)} のようにアロー関数で包むことで、color を引数として渡せる
  • selected にオブジェクトごと保存すると、名前・hex など複数の情報を一度に持てて便利
  • ループ内で動的なスタイルを設定するときは、インラインスタイルにオブジェクトを渡す

解説5:バブリングをstopPropagationで制御しよう

【解答例】

import { useState } from 'react';

function CardWithButton() {
  const [log, setLog] = useState([]);

  const addLog = (msg) => setLog((prev) => [...prev, msg]);

  const handleCardClick = () => {
    addLog("カード全体がクリックされました");
  };

  const handleDetailClick = (e) => {
    e.stopPropagation(); // 親へのバブリングを止める
    addLog("詳細ボタンがクリックされました");
  };

  return (
    <div>
      <div
        onClick={handleCardClick}
        style={{ border: '1px solid #ccc', padding: '20px', cursor: 'pointer' }}
      >
        <p>カードのタイトル</p>
        <button onClick={handleDetailClick}>詳細を見る</button>
      </div>
      <ul>
        {log.map((entry, i) => <li key={i}>{entry}</li>)}
      </ul>
    </div>
  );
}

【解説】

  • e.stopPropagation() を呼ぶと、そのイベントが親要素に伝わらなくなる
  • ログを追加するヘルパー関数 addLog を切り出すと、handleCardClickhandleDetailClick の両方から使い回せる
  • 配列 State の更新は [...prev, 新要素] で行う(直接 push しないこと)

解説6:フォームのデフォルト動作をpreventDefaultで防ごう

【解答例】

import { useState } from 'react';

function SearchForm() {
  const [query,  setQuery]  = useState("");
  const [result, setResult] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault(); // ページリロードを防ぐ
    if (query.trim() === "") return;
    setResult(`「${query.trim()}」を検索しました`);
    setQuery(""); // 入力欄をリセット
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="検索キーワード"
        />
        <button type="submit">検索</button>
      </form>
      {result && <p>{result}</p>}
    </div>
  );
}

【解説】

  • formonSubmit に渡したハンドラーで e.preventDefault() を呼ぶのはフォーム処理の基本パターン
  • button type="submit" を使うと、ボタンクリックでもEnterキーでも onSubmit が発火する
  • フォーム送信処理を onClick ではなく onSubmit で行うのが推奨。アクセシビリティ的にも正しい実装になる

解説7:カスタムボタンコンポーネントにイベントを渡そう

【解答例】

import { useState } from 'react';

function IconButton({ onClick, icon, label }) {
  return (
    <button onClick={onClick} style={{ margin: '4px' }}>
      {icon} {label}
    </button>
  );
}

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>カウント: {count}</p>
      <IconButton icon="➕" label="増やす"  onClick={() => setCount(count + 1)} />
      <IconButton icon="➖" label="減らす"  onClick={() => setCount(count - 1)} />
      <IconButton icon="🔄" label="リセット" onClick={() => setCount(0)} />
    </div>
  );
}

【解説】

  • IconButton は表示だけを担当し、ロジックは親が持つ設計。コンポーネントの役割が明確になる
  • onClick Props を受け取ってそのまま <button> に渡すだけなので、呼び出し側が自由に処理を決められる
  • このパターンは「プレゼンテーションコンポーネント」の典型例。前の記事の内容と合わせて復習しよう

解説8:ホバーで背景色が変わるカードを作ろう

【解答例】

import { useState } from 'react';

const cards = [
  { id: 1, title: "Reactの基礎",  desc: "JSXとコンポーネントを学ぶ" },
  { id: 2, title: "PropsとState", desc: "データの管理方法を理解する" },
  { id: 3, title: "イベント処理",  desc: "インタラクションを実装する" },
];

function HoverCard() {
  const [hoveredId, setHoveredId] = useState(null);

  return (
    <div>
      {cards.map((card) => (
        <div
          key={card.id}
          onMouseEnter={() => setHoveredId(card.id)}
          onMouseLeave={() => setHoveredId(null)}
          style={{
            backgroundColor: hoveredId === card.id ? "#e8f4fd" : "#fff",
            border: '1px solid #ddd',
            borderRadius: '8px',
            padding: '16px',
            margin: '8px 0',
            cursor: 'pointer',
            transition: 'background-color 0.2s',
          }}
        >
          <h3>{card.title}</h3>
          <p>{card.desc}</p>
        </div>
      ))}
    </div>
  );
}

【解説】

  • hoveredId にホバー中のカードのIDを保存し、各カードのレンダリング時に hoveredId === card.id で比較する
  • onMouseLeavenull に戻すことで、ホバーが外れたときに元の色に戻る
  • transition: 'background-color 0.2s' を加えるとスムーズな色の変化になる

解説9:onFocusとonBlurでフォーカス状態を管理しよう

【解答例】

import { useState } from 'react';

function FocusInput() {
  const [isFocused, setIsFocused] = useState(false);
  const [value,     setValue]     = useState("");

  return (
    <div>
      {isFocused && <p>✏️ 入力中...</p>}
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onFocus={() => setIsFocused(true)}
        onBlur={() => setIsFocused(false)}
        style={{
          border: isFocused ? "2px solid #3498db" : "1px solid #ccc",
          padding: '8px',
          borderRadius: '4px',
          outline: 'none',
        }}
        placeholder="クリックしてフォーカスしてください"
      />
      {!isFocused && value && <p>入力値: {value}</p>}
    </div>
  );
}

【解説】

  • onFocusonBlur はペアで使うことが多い。フォーカス時のハイライトやバリデーションに便利
  • outline: 'none' を設定しておかないと、ブラウザのデフォルトの青枠と自前の枠線が重なってしまう
  • !isFocused && value && ... は「フォーカスが外れている かつ 値が空でない」ときだけ表示する二重条件

解説10:キーボードショートカット付きカウンターを実装しよう

【解答例】

import { useState } from 'react';

function KeyboardCounter() {
  const [count, setCount] = useState(0);

  const handleKeyDown = (e) => {
    if (e.key === "ArrowUp")   setCount((prev) => prev + 1);
    if (e.key === "ArrowDown") setCount((prev) => prev - 1);
    if (e.key === "r")         setCount(0);
  };

  return (
    <div
      tabIndex={0}         // フォーカス可能にする
      onKeyDown={handleKeyDown}
      style={{ outline: 'none', padding: '16px', border: '1px solid #ddd' }}
    >
      <p>カウント: {count}</p>
      <button onClick={() => setCount((prev) => prev + 1)}>+1</button>
      <button onClick={() => setCount((prev) => prev - 1)}>−1</button>
      <button onClick={() => setCount(0)}>リセット</button>
      <p style={{ color: '#888', fontSize: '0.9em' }}>
        ショートカット: ↑ +1 / ↓ −1 / R リセット(要素をクリックしてフォーカス)
      </p>
    </div>
  );
}

【解説】

  • tabIndex={0} を付けることで通常フォーカスを受け取れない div もキーボードイベントを受け取れるようになる
  • e.key の値は "ArrowUp""ArrowDown""r" などキーの名前。MDNで一覧を確認できる
  • State 更新に関数形式 prev => prev + 1 を使うことで、連続したキー入力でも正確にカウントできる

解説11:ドラッグ&ドロップ風の並び替えリストを実装しよう

【解答例】

import { useState } from 'react';

function SortableList() {
  const [items,         setItems]         = useState(["React", "JavaScript", "TypeScript", "Node.js", "CSS"]);
  const [selectedIndex, setSelectedIndex] = useState(null);

  const handleItemClick = (index) => {
    if (selectedIndex === null) {
      // 1回目のクリック:選択
      setSelectedIndex(index);
    } else {
      if (selectedIndex === index) {
        // 同じアイテムをクリック:選択解除
        setSelectedIndex(null);
        return;
      }
      // 2回目のクリック:並び替え
      const newItems = items.filter((_, i) => i !== selectedIndex);
      // selectedIndex のアイテムを target の前に挿入
      const targetIndex = index < selectedIndex ? index : index - 1;
      newItems.splice(targetIndex, 0, items[selectedIndex]);
      setItems(newItems);
      setSelectedIndex(null);
    }
  };

  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {items.map((item, index) => (
        <li
          key={item}
          onClick={() => handleItemClick(index)}
          style={{
            backgroundColor: selectedIndex === index ? "#ffeaa7" : "#fff",
            border: '1px solid #ddd',
            padding: '8px 16px',
            margin: '4px 0',
            cursor: 'pointer',
            borderRadius: '4px',
          }}
        >
          {selectedIndex === index ? "✅ " : ""}{item}
          {selectedIndex === null && " (クリックして選択)"}
          {selectedIndex !== null && selectedIndex !== index && " (ここに移動)"}
        </li>
      ))}
    </ul>
  );
}

【解説】

  • selectedIndexnull かどうかで「選択モード」と「移動モード」を切り替える二段階クリックの設計
  • filter() で選択アイテムを除いた新配列を作り、splice() で正しい位置に挿入する。元の配列は変更しない
  • 挿入位置の計算 index < selectedIndex ? index : index - 1 は、選択アイテムを取り除いた後のインデックスのズレを補正している

解説12:アコーディオンUIを実装しよう

【解答例】

import { useState } from 'react';

const faqs = [
  { id: 1, question: "Reactとは何ですか?",      answer: "UIを構築するためのJavaScriptライブラリです。" },
  { id: 2, question: "なぜReactを使うのですか?", answer: "コンポーネントベースの設計で再利用性が高く、効率的な開発が可能です。" },
  { id: 3, question: "Hooksとは何ですか?",       answer: "React 16.8で導入された、関数型コンポーネントで状態管理などを行う仕組みです。" },
];

// 各項目を担当するコンポーネント
function AccordionItem({ question, answer, isOpen, onToggle }) {
  return (
    <div style={{ borderBottom: '1px solid #ddd' }}>
      <div
        onClick={onToggle}
        style={{
          padding: '12px 16px',
          cursor: 'pointer',
          display: 'flex',
          justifyContent: 'space-between',
          fontWeight: 'bold',
        }}
      >
        <span>{question}</span>
        <span>{isOpen ? "▼" : "▶"}</span>
      </div>
      {isOpen && (
        <div style={{ padding: '8px 16px 16px', color: '#555' }}>
          {answer}
        </div>
      )}
    </div>
  );
}

// 全体を管理するコンポーネント
function Accordion() {
  const [openId, setOpenId] = useState(null);

  const handleToggle = (id) => {
    // 同じIDをクリックしたら閉じる、違うIDなら開く
    setOpenId(openId === id ? null : id);
  };

  return (
    <div style={{ border: '1px solid #ddd', borderRadius: '8px', overflow: 'hidden' }}>
      {faqs.map((faq) => (
        <AccordionItem
          key={faq.id}
          question={faq.question}
          answer={faq.answer}
          isOpen={openId === faq.id}
          onToggle={() => handleToggle(faq.id)}
        />
      ))}
    </div>
  );
}

【解説】

  • openId === id ? null : id が「アコーディオンの心臓部」。同じIDなら閉じ(null)、違うIDなら開く(そのID)
  • AccordionItemisOpen という真偽値を受け取るだけで、State の管理は Accordion に任せている。この責務の分離が重要
  • onToggle={() => handleToggle(faq.id)} でアロー関数に包むことで、faq.id を引数として渡している