デザインについての学習メモブログ

React入門 #08 – イベント処理の実装方法

記事内に広告が含まれています。

React入門 #08 – イベント処理の実装方法

ユーザーがボタンをクリックしたり、テキストを入力したりするとイベントが発生します。

Reactでのイベント処理は、HTMLの書き方と似ていますが、重要な違いがあります。

この記事では、Reactのイベント処理を詳しく学んでいきます。

Reactのイベント処理の基本を理解する

Reactのイベント処理の基本

Reactでのイベント処理は、HTMLと似ていますが重要な違いがあります。

このセクションでは、イベント属性の命名規則、基本的な「イベントハンドラの書き方」、「インライン記法」について学びます。

これらはReactでインタラクティブなUIを作る上での土台となる知識です。

イベントの命名規則

HTMLとReactでは、イベント属性の書き方が異なります。

Reactではすべてキャメルケース(camelCase)で記述する必要があります。

この命名規則を守らないと、イベントが正しく動作しません。

HTML と Reactの違い:

JSX
HTMLの場合:  onclick, onchange, onsubmit, onkeydown, onmouseover
Reactの場合: onClick, onChange, onSubmit, onKeyDown, onMouseOver

基本的なイベントハンドラ

イベントハンドラは、コンポーネント内で関数として定義し、JSXの属性に渡します。

6行目の`onClick={handleClick}`がポイントです。

関数を呼び出すのではなく、関数への参照を渡すことに注意してください。

JavaScript版:

JSX
function Button() {
  const handleClick = () => {
    console.log('ボタンがクリックされました')
  }
  
  return <button onClick={handleClick}>クリック</button>
}

TypeScript版:

TSX
function Button(): JSX.Element {
  const handleClick = (): void => {
    console.log('ボタンがクリックされました')
  }
  
  return <button onClick={handleClick}>クリック</button>
}

⚠️ 重要:

`onClick={handleClick()}`のように括弧をつけると、レンダリング時に即座に実行されてしまいます。

インラインでイベントハンドラを書く

シンプルな処理の場合、アロー関数を使ってインラインでイベントハンドラを記述できます。

3行目の`onClick={() => console.log(‘クリック’)}`のように、JSX内に直接関数を定義します。

簡潔ですが、再レンダリングのたびに新しい関数が作成される点に注意が必要です。

JavaScript版:

JSX
function Button() {
  return (
    <button onClick={() => console.log('クリック')}>
      クリック
    </button>
  )
}

TypeScript版:

TSX
function Button(): JSX.Element {
  return (
    <button onClick={() => console.log('クリック')}>
      クリック
    </button>
  )
}

よく使うイベント

実際の開発で頻繁に使う代表的なイベントを、実践的なコード例とともに学びます。

クリック、入力、フォーム送信、フォーカス、キーボード、マウスイベントなど、日常的に扱うイベント処理をマスターしましょう。

クリックイベント(onClick)

ボタンのクリックを検知する最も基本的なイベントです。

9-11行目でクリック時にカウントを更新し、12-14行目でリセットボタンを実装しています。

useStateと組み合わせることで、インタラクティブなUIが実現できます。

JavaScript版:

JSX
import { useState } from 'react'

function ClickExample() {
  const [count, setCount] = useState(0)
  
  return (
    <div>
      <p>クリック数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
      <button onClick={() => setCount(0)}>
        リセット
      </button>
    </div>
  )
}

TypeScript版:

TSX
import { useState } from 'react'

function ClickExample(): JSX.Element {
  const [count, setCount] = useState<number>(0)
  
  return (
    <div>
      <p>クリック数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
      <button onClick={() => setCount(0)}>
        リセット
      </button>
    </div>
  )
}

変更イベント(onChange)

入力フィールドの値が変更されるたびに発火するイベントです。

7行目の`e.target.value`で入力値を取得し、stateを更新します。

この仕組みで「制御されたコンポーネント」を実現でき、入力値をReactで完全に管理できます。

⚠️ `e`(イベントオブジェクト)については、そのセクションで詳しく解説します。

JavaScript版:

JSX
import { useState } from 'react'

function TextInput() {
  const [text, setText] = useState('')
  
  const handleChange = (e) => {
    setText(e.target.value)
  }
  
  return (
    <div>
      <input
        type="text"
        value={text}
        onChange={handleChange}
        placeholder="入力してください"
      />
      <p>入力内容: {text}</p>
    </div>
  )
}

TypeScript版:

TSX
import { useState } from 'react'

function TextInput(): JSX.Element {
  const [text, setText] = useState<string>('')
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    setText(e.target.value)
  }
  
  return (
    <div>
      <input
        type="text"
        value={text}
        onChange={handleChange}
        placeholder="入力してください"
      />
      <p>入力内容: {text}</p>
    </div>
  )
}

送信イベント(onSubmit)

フォーム送信時に発火するイベントです。

8行目の`e.preventDefault()`が最も重要で、これによりページのリロードを防ぎます。

Reactでフォームを扱う際は、ほぼ必須のテクニックです。

JavaScript版:

JSX
import { useState } from 'react'

function Form() {
  const [name, setName] = useState('')
  const [submitted, setSubmitted] = useState(false)
  
  const handleSubmit = (e) => {
    e.preventDefault()  // デフォルトの動作をキャンセル
    setSubmitted(true)
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="名前を入力"
      />
      <button type="submit">送信</button>
      
      {submitted && <p>送信完了:{name}</p>}
    </form>
  )
}

TypeScript版:

TSX
import { useState } from 'react'

function Form(): JSX.Element {
  const [name, setName] = useState<string>('')
  const [submitted, setSubmitted] = useState<boolean>(false)
  
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault()
    setSubmitted(true)
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="名前を入力"
      />
      <button type="submit">送信</button>
      
      {submitted && <p>送信完了:{name}</p>}
    </form>
  )
}

フォーカスイベント(onFocus, onBlur)

入力フィールドにフォーカスが当たった時(onFocus)と外れた時(onBlur)に発火します。

10行目と11行目でフォーカス状態を切り替えています。

バリデーションメッセージの表示/非表示などに活用できます。

JavaScript版:

JSX
import { useState } from 'react'

function FocusExample() {
  const [isFocused, setIsFocused] = useState(false)
  
  return (
    <div>
      <input
        type="text"
        onFocus={() => setIsFocused(true)}
        onBlur={() => setIsFocused(false)}
        placeholder="フォーカスしてみて"
      />
      <p>フォーカス状態: {isFocused ? 'あり' : 'なし'}</p>
    </div>
  )
}

TypeScript版:

TSX
import { useState } from 'react'

function FocusExample(): JSX.Element {
  const [isFocused, setIsFocused] = useState<boolean>(false)
  
  return (
    <div>
      <input
        type="text"
        onFocus={() => setIsFocused(true)}
        onBlur={() => setIsFocused(false)}
        placeholder="フォーカスしてみて"
      />
      <p>フォーカス状態: {isFocused ? 'あり' : 'なし'}</p>
    </div>
  )
}

キーボードイベント(onKeyDown, onKeyUp, onKeyPress)

キーボードの入力を検知するイベントです。

7行目の`e.key`でどのキーが押されたかを判定できます。

下の例の37行目のonKeyPressでは、Enterキーのみを検出して検索を実行しています。

JavaScript版:

JSX
import { useState } from 'react'

function KeyboardExample() {
  const [key, setKey] = useState('')
  
  const handleKeyDown = (e) => {
    setKey(`キーが押された: ${e.key}`)
  }
  
  return (
    <div>
      <input
        type="text"
        onKeyDown={handleKeyDown}
        placeholder="キーを押してください"
      />
      <p>{key}</p>
    </div>
  )
}

// Enterキーのみを検出
function SearchBox() {
  const [query, setQuery] = useState('')
  
  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      console.log('検索:', query)
    }
  }
  
  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onKeyPress={handleKeyPress}
      placeholder="検索"
    />
  )
}

TypeScript版:

TSX
import { useState } from 'react'

function KeyboardExample(): JSX.Element {
  const [key, setKey] = useState<string>('')
  
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
    setKey(`キーが押された: ${e.key}`)
  }
  
  return (
    <div>
      <input
        type="text"
        onKeyDown={handleKeyDown}
        placeholder="キーを押してください"
      />
      <p>{key}</p>
    </div>
  )
}

function SearchBox(): JSX.Element {
  const [query, setQuery] = useState<string>('')
  
  const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>): void => {
    if (e.key === 'Enter') {
      console.log('検索:', query)
    }
  }
  
  return (
    <input
      type="text"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      onKeyPress={handleKeyPress}
      placeholder="検索"
    />
  )
}

マウスイベント(onMouseOver, onMouseEnter, onMouseLeave)

マウスカーソルの動きを検知するイベントです。

8行目と9行目でホバー状態を管理し、11-13行目で背景色を動的に変更しています。

ツールチップやドロップダウンメニューの実装に活用できます。

JavaScript版:

JSX
import { useState } from 'react'

function HoverExample() {
  const [isHovered, setIsHovered] = useState(false)
  
  return (
    <div
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      style={{
        backgroundColor: isHovered ? 'lightblue' : 'white',
        padding: '20px',
        transition: 'background-color 0.3s'
      }}
    >
      {isHovered ? 'ホバー中' : 'ホバーしてみて'}
    </div>
  )
}

TypeScript版:

TSX
import { useState } from 'react'

function HoverExample(): JSX.Element {
  const [isHovered, setIsHovered] = useState<boolean>(false)
  
  return (
    <div
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      style={{
        backgroundColor: isHovered ? 'lightblue' : 'white',
        padding: '20px',
        transition: 'background-color 0.3s'
      }}
    >
      {isHovered ? 'ホバー中' : 'ホバーしてみて'}
    </div>
  )
}

イベントオブジェクト

イベントハンドラには自動的に「イベントオブジェクト」が渡されます。

このセクションでは、イベントオブジェクトから必要な情報を取得する方法、デフォルト動作のキャンセル、イベント伝播の制御など、より高度なイベント制御のテクニックを学びます。

イベントオブジェクトの情報を使う

イベントハンドラの第一引数として渡される`e`(イベントオブジェクト)には、イベントに関する様々な情報が含まれています。

3-7行目で、イベントの種類、ターゲット要素、マウス位置、修飾キーなどを取得できます。

JavaScript版:

JSX
function EventObjectExample() {
  const handleClick = (e) => {
    console.log('イベント種別:', e.type)
    console.log('ターゲット:', e.target)
    console.log('ターゲットの値:', e.target.value)
    console.log('マウス位置:', e.clientX, e.clientY)
    console.log('修飾キー:', e.ctrlKey, e.shiftKey, e.altKey)
  }
  
  return (
    <div>
      <button onClick={handleClick}>クリック</button>
      <input onChange={handleClick} placeholder="入力" />
    </div>
  )
}

TypeScript版:

TSX
function EventObjectExample(): JSX.Element {
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
    console.log('イベント種別:', e.type)
    console.log('ターゲット:', e.currentTarget)
    console.log('マウス位置:', e.clientX, e.clientY)
  }
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    console.log('入力値:', e.target.value)
  }
  
  return (
    <div>
      <button onClick={handleClick}>クリック</button>
      <input onChange={handleChange} placeholder="入力" />
    </div>
  )
}

e.preventDefault() – デフォルト動作をキャンセル

ブラウザのデフォルト動作(リンクのページ遷移、フォームのリロードなど)をキャンセルします。

3行目と9行目の`e.preventDefault()`がポイントです。

Reactでフォームやリンクを扱う際の必須テクニックです。

JavaScript版:

JSX
function PreventDefaultExample() {
  const handleLinkClick = (e) => {
    e.preventDefault()  // ページ遷移を防ぐ

    console.log('リンククリック処理')
  }
  
  const handleFormSubmit = (e) => {
    e.preventDefault()  // ページリロードを防ぐ
    console.log('フォーム送信処理')
  }
  
  return (
    <div>
      <a href="https://example.com" onClick={handleLinkClick}>
        リンク
      </a>
      
      <form onSubmit={handleFormSubmit}>
        <input type="text" />
        <button type="submit">送信</button>
      </form>
    </div>
  )
}

使用例:

「SPAでのルーティング」、「Ajaxによるフォーム送信」、「カスタムリンク動作の実装」など。

e.stopPropagation() – イベント伝播を停止

イベントは通常、子要素から親要素へと伝播(バブリング)します。

7行目の`e.stopPropagation()`で、この伝播を止めることができます。

親要素のクリックイベントが発火しないようにしたい場合に使用します。

JavaScript版:

JSX
function StopPropagationExample() {
  const handleParentClick = () => {
    console.log('親がクリックされた')
  }
  
  const handleChildClick = (e) => {
    e.stopPropagation()  // イベント伝播を停止
    console.log('子がクリックされた')
  }
  
  return (
    <div onClick={handleParentClick} style={{ border: '1px solid black', padding: '20px' }}>
      <p>親要素</p>
      <button onClick={handleChildClick}>
        子要素(クリックしても親には伝播しない)
      </button>
    </div>
  )
}

使用例:

モーダルの背景クリック時の動作制御、ネストされた要素での個別クリック処理など。

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

リストアイテムの削除など、イベントハンドラに追加の情報(IDやインデックスなど)を渡したい場面は頻繁にあります。

このセクションでは、アロー関数やbind()を使って、イベントハンドラに引数を渡す2つの方法を学びます。

方法1:アロー関数で囲む

最も一般的で直感的な方法です。

16行目のように、アロー関数でイベントハンドラを囲み、その中で引数付きの関数を呼び出します。

各リストアイテムに対して、そのインデックスを渡すことができます。

JavaScript版:

JSX
import { useState } from 'react'

function TodoList() {
  const [todos, setTodos] = useState(['買い物', '洗濯', '掃除'])
  
  const handleDelete = (index) => {
    setTodos(todos.filter((_, i) => i !== index))
  }
  
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>
          {todo}
          {/* アロー関数で引数を渡す */}
          <button onClick={() => handleDelete(index)}>
            削除
          </button>
        </li>
      ))}
    </ul>
  )
}

TypeScript版:

TSX
import { useState } from 'react'

function TodoList(): JSX.Element {
  const [todos, setTodos] = useState<string[]>(['買い物', '洗濯', '掃除'])
  
  const handleDelete = (index: number): void => {
    setTodos(todos.filter((_, i) => i !== index))
  }
  
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>
          {todo}
           {/* アロー関数で引数を渡す */}
          <button onClick={() => handleDelete(index)}>
            削除
          </button>
        </li>
      ))}
    </ul>
  )
}

方法2:bind()を使う

クラスコンポーネントでよく使われる方法です。

15行目の`this.handleClick.bind(this, 1)`で、関数に`this`と引数を束縛します。

現在は関数コンポーネントが主流なので、方法1の方が一般的です。

JavaScript版:

JSX
class Counter extends React.Component {
  constructor(props) {
    super(props)
    this.state = { count: 0 }
  }
  
  handleClick(value) {
    this.setState({ count: this.state.count + value })
  }
  
  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={this.handleClick.bind(this, 1)}>
          +1
        </button>
      </div>
    )
  }
}

複数のイベントハンドラを組み合わせる

実際のフォームでは、複数の入力フィールドを一つのstateオブジェクトで管理することがよくあります。

このセクションでは、複数のイベントハンドラを効率的に組み合わせて、実用的なフォームを構築する方法を学びます。

複数の入力フィールドを一つの関数で

複数の入力フィールドを一つの関数で処理するテクニックです。

14行目の`[name]`は計算されたプロパティ名で、動的にstateのキーを更新できます。

「チェックボックス」と「テキスト入力」を同じ関数(handleInputChange)で処理しています。

JavaScript版:

JSX
import { useState } from 'react'

function AdvancedForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    rememberMe: false
  })
  
  const handleInputChange = (e) => {
    const { name, value, type, checked } = e.target
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    })
  }
  
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('フォームデータ:', formData)
  }
  
  const handleReset = () => {
    setFormData({
      email: '',
      password: '',
      rememberMe: false
    })
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleInputChange}
        placeholder="メール"
      />
      
      <input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleInputChange}
        placeholder="パスワード"
      />
      
      <label>
        <input
          type="checkbox"
          name="rememberMe"
          checked={formData.rememberMe}
          onChange={handleInputChange}
        />
        ログイン状態を保存
      </label>
      
      <button type="submit">ログイン</button>
      <button type="button" onClick={handleReset}>
        リセット
      </button>
    </form>
  )
}

ポイント:

  • name属性が各フィールドの識別子として機能
  • type === ‘checkbox’でチェックボックスとテキスト入力を分岐
  • 一つの関数で全ての入力を処理できるため、コードが簡潔に

TypeScript版:

TSX
import { useState } from 'react'

interface FormData {
  email: string
  password: string
  rememberMe: boolean
}

function AdvancedForm(): JSX.Element {
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: '',
    rememberMe: false
  })
  
  const handleInputChange = (
    e: React.ChangeEvent<HTMLInputElement>
  ): void => {
    const { name, value, type, checked } = e.currentTarget
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    })
  }
  
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault()
    console.log('フォームデータ:', formData)
  }
  
  const handleReset = (): void => {
    setFormData({
      email: '',
      password: '',
      rememberMe: false
    })
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleInputChange}
        placeholder="メール"
      />
      
      <input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleInputChange}
        placeholder="パスワード"
      />
      
      <label>
        <input
          type="checkbox"
          name="rememberMe"
          checked={formData.rememberMe}
          onChange={handleInputChange}
        />
        ログイン状態を保存
      </label>
      
      <button type="submit">ログイン</button>
      <button type="button" onClick={handleReset}>
        リセット
      </button>
    </form>
  )
}

イベント委譲(Event Delegation)

大量の要素に個別にイベントハンドラを設定するのは非効率です。

このセクションでは、親要素で一括してイベントを処理する「イベント委譲」のテクニックを学びます。

ただし、Reactでは他の方法が推奨される場合もあります。

大量の要素にイベントハンドラを付ける場合

親要素でまとめてイベントを処理することで、メモリ使用量を削減できます。

13行目で、クリックされた要素がボタンかどうかを判定し、14行目のdata-id属性からIDを取得しています。

ただし、Reactでは通常、各要素に直接ハンドラを設定する方が推奨されます。

JavaScript版:

JSX
import { useState } from 'react'

function ItemList() {
  const [items] = useState([
    { id: 1, name: 'りんご' },
    { id: 2, name: 'バナナ' },
    { id: 3, name: 'オレンジ' },
    { id: 4, name: 'ブドウ' }
  ])
  
  // イベント委譲:親要素で処理
  const handleListClick = (e) => {
    if (e.target.tagName === 'BUTTON') {
      const id = e.target.getAttribute('data-id')
      console.log(`ID ${id} が削除されました`)
    }
  }
  
  return (
    <ul onClick={handleListClick}>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          <button data-id={item.id}>削除</button>
        </li>
      ))}
    </ul>
  )
}

TypeScript版:

TSX
import { useState } from 'react'

interface Item {
  id: number
  name: string
}

function ItemList(): JSX.Element {
  const [items] = useState<Item[]>([
    { id: 1, name: 'りんご' },
    { id: 2, name: 'バナナ' },
    { id: 3, name: 'オレンジ' },
    { id: 4, name: 'ブドウ' }
  ])
  
  const handleListClick = (e: React.MouseEvent<HTMLUListElement>): void => {
    const target = e.target as HTMLElement
    if (target.tagName === 'BUTTON') {
      const id = target.getAttribute('data-id')
      console.log(`ID ${id} が削除されました`)
    }
  }
  
  return (
    <ul onClick={handleListClick}>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          <button data-id={item.id}>削除</button>
        </li>
      ))}
    </ul>
  )
}

⚠️ 注意:

この方法はDOM操作が中心のバニラJSでは有効ですが、Reactでは以下の理由から推奨されません

  • コードが読みにくくなる
  • 型安全性が損なわれる
  • 各要素に直接ハンドラを設定しても、Reactの内部最適化により性能面の問題はほぼない

よくあるイベント処理の間違い

初心者がつまずきやすい典型的なミスを事前に知っておくことで、デバッグ時間を大幅に短縮できます。

このセクションでは、関数の即座実行、キャメルケースの誤り、stateの参照タイミングなど、よくある間違いとその解決方法を学びます。

間違い1:関数の呼び出し(括弧をつける)

最も頻繁に起こる間違いです。

2行目の`handleClick()`のように括弧をつけると、レンダリング時に即座に実行されてしまい、クリック時には動作しません。

括弧を外して関数への参照を渡すことが重要です。

❌ 悪い例:

JSX
<button onClick={handleClick()}>
  {/* handleClickが即座に実行される */}
  クリック
</button>

✅ 良い例:

JSX
<button onClick={handleClick}>
  {/* クリック時にhandleClickが実行される */}
  クリック
</button>

覚え方:

「今すぐ実行したい」なら括弧あり、「後で実行したい」なら括弧なし。

イベントハンドラは常に後で」なので括弧なし。

間違い2:キャメルケースを忘れる

HTMLの書き方をそのまま使ってしまう間違いです。

2行目と3行目のように小文字だけで書くと、Reactでは動作しません。

必ず属性名をキャメルケース(onClick、onChange)を使いましょう。

❌ 悪い例:

JSX
          ↓
<button onclick={handleClick}>クリック</button> 
<input onchange={handleChange} />  

✅ 良い例:

JSX
<button onClick={handleClick}>クリック</button>
<input onChange={handleChange} />

💡 ポイント:

VSCodeなどのエディタを使っていれば、自動補完でキャメルケースが提案されるので、それに従うと安全です。

間違い3:e.target を使わずにstateを参照

stateの更新は非同期なので、イベントハンドラ内でstateを参照すると古い値になることがあります。

8行目のように現在のstateを参照するのではなく、17行目の`e.target.value`で直接入力値を取得すべきです。

❌ 悪い例:

JSX
function Input() {
  const [value, setValue] = useState('')
  
  const handleChange = () => {
    console.log(value)  // 古い値が表示される
  }
  
  return <input onChange={handleChange} />
}

✅ 良い例:

JSX
function Input() {
  const [value, setValue] = useState('')
  
  const handleChange = (e) => {
    const newValue = e.target.value
    setValue(newValue)
    console.log(newValue)  // 新しい値が表示される
  }
  
  return <input onChange={handleChange} />
}

重要: イベントハンドラでは、常に`e.target`から値を取得するのが確実です。

Reactのイベント処理の実践的テクニック

実践:インタラクティブなコンポーネント

これまで学んだ知識を総動員して、複数のイベント処理を組み合わせた実践的なコンポーネントを作ります。

展開/折りたたみ、いいね、コメント機能など、実際のアプリケーションでよく見る機能を実装しながら、イベント処理の実践力を身につけましょう。

JavaScript版:

JSX
import { useState } from 'react'

function InteractiveCard() {
  const [isExpanded, setIsExpanded] = useState(false)
  const [likes, setLikes] = useState(0)
  const [comments, setComments] = useState([])
  const [newComment, setNewComment] = useState('')
  
  const handleToggle = () => {
    setIsExpanded(!isExpanded)
  }
  
  const handleLike = () => {
    setLikes(likes + 1)
  }
  
  const handleCommentChange = (e) => {
    setNewComment(e.target.value)
  }
  
  const handleCommentSubmit = (e) => {
    e.preventDefault()
    if (newComment.trim()) {
      setComments([...comments, newComment])
      setNewComment('')
    }
  }
  
  const handleDeleteComment = (index) => {
    setComments(comments.filter((_, i) => i !== index))
  }
  
  return (
    <div className="card">
      <div className="card-header">
        <h2>React入門</h2>
        <button onClick={handleToggle} className="toggle-btn">
          {isExpanded ? '閉じる' : '開く'}
        </button>
      </div>
      
      {isExpanded && (
        <div className="card-body">
          <p>
            Reactは、ユーザーインターフェースを構築するための
            JavaScriptライブラリです。
          </p>
          
          <button onClick={handleLike} className="like-btn">
            ❤️ {likes}
          </button>
          
          <div className="comments-section">
            <h3>コメント ({comments.length})</h3>
            
            <form onSubmit={handleCommentSubmit}>
              <input
                type="text"
                value={newComment}
                onChange={handleCommentChange}
                onKeyPress={(e) => {
                  if (e.key === 'Enter') {
                    handleCommentSubmit(e)
                  }
                }}
                placeholder="コメントを入力"
              />
              <button type="submit">コメント</button>
            </form>
            
            <ul className="comments-list">
              {comments.map((comment, index) => (
                <li key={index}>
                  <span>{comment}</span>
                  <button 
                    onClick={() => handleDeleteComment(index)}
                    className="delete-btn"
                  >
                    削除
                  </button>
                </li>
              ))}
            </ul>
          </div>
        </div>
      )}
    </div>
  )
}

export default InteractiveCard

💡 着目したいポイント

記事前半で学んだ内容が凝縮された総まとめです。

  • handleToggle(9行目)→ セクション2で学んだonClickの応用
  • handleCommentChange(17行目)→ セクション2のonChangeそのまま
  • e.preventDefault()(22行目)→ セクション3で学んだフォームのリロード防止
  • () => handleDeleteComment(index)(76行目)→ セクション4のアロー関数で引数を渡す方法
  • e.key === ‘Enter’(62行目)→ セクション2のキーボードイベント

TypeScript版:

TSX
import { useState, type JSX } from 'react'

function InteractiveCard(): JSX.Element {
  const [isExpanded, setIsExpanded] = useState<boolean>(false)
  const [likes, setLikes] = useState<number>(0)
  const [comments, setComments] = useState<string[]>([])
  const [newComment, setNewComment] = useState<string>('')
  
  const handleToggle = (): void => {
    setIsExpanded(!isExpanded)
  }
  
  const handleLike = (): void => {
    setLikes(likes + 1)
  }
  
  const handleCommentChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    setNewComment(e.target.value)
  }
  
  const handleCommentSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault()
    if (newComment.trim()) {
      setComments([...comments, newComment])
      setNewComment('')
    }
  }
  
  const handleDeleteComment = (index: number): void => {
    setComments(comments.filter((_, i) => i !== index))
  }
  
  const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>): void => {
    if (e.key === 'Enter') {
      handleCommentSubmit(e as unknown as React.FormEvent<HTMLFormElement>)
    }
  }
  
  return (
    <div className="card">
      <div className="card-header">
        <h2>React入門</h2>
        <button onClick={handleToggle} className="toggle-btn">
          {isExpanded ? '閉じる' : '開く'}
        </button>
      </div>
      
      {isExpanded && (
        <div className="card-body">
          <p>
            Reactは、ユーザーインターフェースを構築するための
            JavaScriptライブラリです。
          </p>
          
          <button onClick={handleLike} className="like-btn">
            ❤️ {likes}
          </button>
          
          <div className="comments-section">
            <h3>コメント ({comments.length})</h3>
            
            <form onSubmit={handleCommentSubmit}>
              <input
                type="text"
                value={newComment}
                onChange={handleCommentChange}
                onKeyPress={handleKeyPress}
                placeholder="コメントを入力"
              />
              <button type="submit">コメント</button>
            </form>
            
            <ul className="comments-list">
              {comments.map((comment, index) => (
                <li key={index}>
                  <span>{comment}</span>
                  <button 
                    onClick={() => handleDeleteComment(index)}
                    className="delete-btn"
                  >
                    削除
                  </button>
                </li>
              ))}
            </ul>
          </div>
        </div>
      )}
    </div>
  )
}

export default InteractiveCard

利用可能なイベントの一覧

Reactで使用できるイベントは非常に多岐にわたります。

このセクションでは、フォーム、マウス、キーボード、その他のカテゴリ別に、主要なイベントをリファレンスとして一覧で紹介します。

必要に応じて参照してください。

✍️ フォーム関連イベント

ユーザー入力を扱う際に使用するイベント群です。

最も頻繁に使用するのはonChange(入力値の変更)とonSubmit(フォーム送信)です。

  • onChange: 入力値が変わった
  • onSubmit: フォーム送信
  • onReset: フォームリセット
  • onInput: テキスト入力
  • onFocus: フォーカス
  • onBlur: フォーカス喪失

🖱️ マウス関連イベント

マウス操作を検知するイベント群です。

ボタンクリック(onClick)やホバー効果(onMouseEnter/onMouseLeave)でよく使います。

  • onClick: クリック
  • onDoubleClick: ダブルクリック
  • onMouseEnter: マウス進入
  • onMouseLeave: マウス退出
  • onMouseOver: マウスオーバー
  • onMouseOut: マウスアウト
  • onMouseDown: マウスボタン押下
  • onMouseUp: マウスボタン解放
  • onMouseMove: マウス移動

⌨️ キーボード関連イベント

キーボード入力を検知するイベント群です。onKeyDownが最も汎用的で推奨されます。

  • onKeyDown: キー押下
  • onKeyUp: キー解放
  • onKeyPress: キー入力(非推奨)

🫳 その他のイベント

スクロールやドラッグ&ドロップなど、特殊な操作を扱うイベント群です。

  • onScroll: スクロール
  • onWheel: マウスホイール
  • onDrag: ドラッグ中
  • onDrop: ドロップ
  • onTouchStart: タッチ開始
  • onTouchEnd: タッチ終了
  • onContextMenu: 右クリック

イベントのパフォーマンス最適化

検索入力やスクロールなど、連続して発火するイベントは、そのまま処理するとパフォーマンスの問題を引き起こします。

このセクションでは、デバウンススロットルという2つの最適化テクニックを学び、快適なユーザー体験を実現する方法を習得します。

デバウンス(Debounce)

連続するイベントの最後の1回だけを実行するテクニックです。

debounce関数が、タイマーをリセットしながら最終的な入力だけを処理します。

検索フィールドで「入力が終わってから検索」を実現するのに最適です。

JavaScript版:

JSX
import { useState } from 'react'

function SearchWithDebounce() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  
  // デバウンス実装
  const debounce = (func, delay) => {
    let timeoutId
    return (...args) => {
      clearTimeout(timeoutId)
      timeoutId = setTimeout(() => func(...args), delay)
    }
  }
  
  const handleSearch = debounce((searchTerm) => {
    if (searchTerm) {
      console.log('検索:', searchTerm)
      // API呼び出しなど
      setResults([`${searchTerm}に関する結果1`, `${searchTerm}に関する結果2`])
    }
  }, 500)
  
  const handleChange = (e) => {
    const value = e.target.value
    setQuery(value)
    handleSearch(value)
  }
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="検索キーワードを入力"
      />
      <ul>
        {results.map((result, index) => (
          <li key={index}>{result}</li>
        ))}
      </ul>
    </div>
  )
}

使用例:
検索フィールド、自動保存機能、ウィンドウリサイズ処理など

効果:
ユーザーが入力を止めてから500ms後に1回だけ検索が実行されるため、APIへの無駄なリクエストを大幅に削減できます。

TypeScript版:

TSX
import { useState } from 'react'

function SearchWithDebounce(): JSX.Element {
  const [query, setQuery] = useState<string>('')
  const [results, setResults] = useState<string[]>([])
  
  const debounce = <T extends any[]>(
    func: (...args: T) => void,
    delay: number
  ) => {
    let timeoutId: NodeJS.Timeout
    return (...args: T) => {
      clearTimeout(timeoutId)
      timeoutId = setTimeout(() => func(...args), delay)
    }
  }
  
  const handleSearch = debounce((searchTerm: string) => {
    if (searchTerm) {
      console.log('検索:', searchTerm)
      setResults([`${searchTerm}に関する結果1`, `${searchTerm}に関する結果2`])
    }
  }, 500)
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
    const value = e.target.value
    setQuery(value)
    handleSearch(value)
  }
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="検索キーワードを入力"
      />
      <ul>
        {results.map((result, index) => (
          <li key={index}>{result}</li>
        ))}
      </ul>
    </div>
  )
}

スロットル(Throttle)

一定時間内に1回だけ実行するよう制限するテクニックです。

throttle関数が、フラグ(inThrottle)を使って実行頻度を制御します。

スクロールやマウス移動など、高頻度で発火するイベントに最適です。

JavaScript版:

JSX
import { useState } from 'react'

function ScrollWithThrottle() {
  const [scrollCount, setScrollCount] = useState(0)
  
  const throttle = (func, limit) => {
    let inThrottle
    return (...args) => {
      if (!inThrottle) {
        func(...args)
        inThrottle = true
        setTimeout(() => (inThrottle = false), limit)
      }
    }
  }
  
  const handleScroll = throttle(() => {
    setScrollCount(prev => prev + 1)
    console.log('スクロール検出')
  }, 500)
  
  return (
    <div 
      onScroll={handleScroll}
      style={{ height: '300px', overflow: 'auto', border: '1px solid gray' }}
    >
      <div style={{ height: '1000px' }}>
        <p>スクロール検出回数: {scrollCount}</p>
        <p>このエリアをスクロールしてください</p>
      </div>
    </div>
  )
}

使用例:
スクロール検出、マウス移動トラッキング、アニメーション制御など

効果:
スクロール中も500msごとに1回だけ処理が実行されるため、パフォーマンスを維持しながらリアルタイム性も確保できます。

TypeScript版:

TSX
import { useState } from 'react'

function ScrollWithThrottle(): JSX.Element {
  const [scrollCount, setScrollCount] = useState<number>(0)
  
  const throttle = <T extends any[]>(
    func: (...args: T) => void,
    limit: number
  ) => {
    let inThrottle: boolean = false
    return (...args: T) => {
      if (!inThrottle) {
        func(...args)
        inThrottle = true
        setTimeout(() => (inThrottle = false), limit)
      }
    }
  }
  
  const handleScroll = throttle(() => {
    setScrollCount(prev => prev + 1)
    console.log('スクロール検出')
  }, 500)
  
  return (
    <div 
      onScroll={handleScroll}
      style={{ height: '300px', overflow: 'auto', border: '1px solid gray' }}
    >
      <div style={{ height: '1000px' }}>
        <p>スクロール検出回数: {scrollCount}</p>
        <p>このエリアをスクロールしてください</p>
      </div>
    </div>
  )
}

デバウンスとスロットルの使い分け:

  • デバウンス: 「最後の1回」だけ実行 → 検索入力、自動保存
  • スロットル: 「一定間隔で」実行 → スクロール、マウス移動

イベント処理のベストプラクティス

コードの品質を高めるための実践的なガイドラインを学びます。

意味のある命名、適切な関数配置、効率的な情報抽出、明確なコメントなど、プロフェッショナルなコードを書くための4つの重要なプラクティスを身につけましょう。

プラクティス1: 意味のある関数名をつける

イベントハンドラの名前は、「何が起きるか」を明確に表現すべきです。

汎用的すぎる名前は避け、具体的な動作を示す名前を使いましょう。

❌ 悪い例:

JSX
const handle = () => { ... }          // ← 何を処理するのか不明
const handleEvent = () => { ... }    // ← 汎用的すぎる
const onClick = () => { ... }        // ← イベント名そのままで意味がない

✅ 良い例:

JSX
const handleUserLogin = () => { ... }      // ← ユーザーログイン処理とわかる
const handleFormSubmit = () => { ... }     // ← フォーム送信とわかる
const handleItemDelete = () => { ... }     // ← アイテム削除とわかる

命名規則:

`handle + 対象 + 動作`の形式が推奨されます
(例: handleCommentSubmit、handleProfileUpdate)。

プラクティス2: イベントハンドラをコンポーネント外で定義

インライン関数は毎回新しい関数を作成するため、再レンダリングのたびにメモリを消費します。

関数を外で定義することで、パフォーマンスが向上します。

❌ 悪い例(毎回新しい関数が作成される):

JSX
function App() {
  return (
    <button onClick={() => console.log('クリック')}>// ← 再レンダリングごとに新しい関数
      クリック
    </button>
  )
}

✅ 良い例:

JSX
const handleClick = () => {// ← 1回だけ定義
  console.log('クリック')
}

function App() {
  return <button onClick={handleClick}>クリック</button>// ← 同じ関数を再利用
}

例外:

引数を渡す必要がある場合は、アロー関数を使うのが一般的です(`onClick={() => handleDelete(id)}`)。

プラクティス3: イベントオブジェクトは必要な情報だけを抽出

“イベントオブジェクト(e)”全体を扱うのではなく、必要なプロパティだけを分割代入で取り出すことで、コードが読みやすくなります。

❌ 悪い例:

JSX
const handleChange = (e) => {
  console.log(e)  // 全体をログに出力
}

✅ 良い例:

JSX
const handleChange = (e) => {
  const { value, name } = e.target// ← 必要な情報だけを抽出
  console.log(`${name}${value}に変更されました`)
}

プラクティス4: デフォルト動作をキャンセルしたら理由をコメントする

`e.preventDefault()`を使う際は、なぜデフォルト動作をキャンセルするのかコメントで明記しましょう。

コードの意図が明確になります。

JavaScript版:

JSX
const handleLinkClick = (e) => {
  e.preventDefault()  // ページ遷移を防ぎ、カスタム処理を実行
  navigateCustom(e.target.href)
}

const handleFormSubmit = (e) => {
  e.preventDefault()  // ページリロードを防ぎ、APIで送信
  submitFormWithAPI(formData)
}

ポイント:

将来の自分や他の開発者が、なぜpreventDefaultが必要なのかすぐに理解できるようにします。

実践:複雑なフォーム

最後に、バリデーション、エラー処理、ローディング状態、複数の入力タイプなど、実務で必要になるすべての要素を盛り込んだ本格的なフォームを実装します。

これまでの学習の集大成として、実践的なスキルを確立しましょう。

JavaScript版:

JSX
import { useState } from 'react'

function ComplexForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    agreeTerms: false,
    country: ''
  })
  
  const [errors, setErrors] = useState({})
  const [submitted, setSubmitted] = useState(false)
  const [isLoading, setIsLoading] = useState(false)
  
  // 入力値の変更
  const handleInputChange = (e) => {
    const { name, value, type, checked } = e.target
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    })
    // 入力時にエラーをクリア
    if (errors[name]) {
      setErrors({
        ...errors,
        [name]: ''
      })
    }
  }
  
  // バリデーション
  const validateForm = () => {
    const newErrors = {}
    
    if (!formData.username.trim()) {
      newErrors.username = 'ユーザー名は必須です'
    }
    
    if (!formData.email.includes('@')) {
      newErrors.email = '有効なメールアドレスを入力してください'
    }
    
    if (formData.password.length < 8) {
      newErrors.password = 'パスワードは8文字以上である必要があります'
    }
    
    if (formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'パスワードが一致しません'
    }
    
    if (!formData.agreeTerms) {
      newErrors.agreeTerms = '利用規約に同意してください'
    }
    
    return newErrors
  }
  
  // フォーム送信
  const handleSubmit = async (e) => {
    e.preventDefault()
    
    const newErrors = validateForm()
    setErrors(newErrors)
    
    if (Object.keys(newErrors).length === 0) {
      setIsLoading(true)
      try {
        <em>// API呼び出しをシミュレート</em>
        await new Promise(resolve => setTimeout(resolve, 1000))
        setSubmitted(true)
        console.log('フォーム送信成功:', formData)
      } catch (error) {
        console.error('送信エラー:', error)
      } finally {
        setIsLoading(false)
      }
    }
  }
  
  // フォームリセット
  const handleReset = () => {
    setFormData({
      username: '',
      email: '',
      password: '',
      confirmPassword: '',
      agreeTerms: false,
      country: ''
    })
    setErrors({})
    setSubmitted(false)
  }
  
  if (submitted) {
    return (
      <div className="success-message">
        <h2>登録が完了しました!</h2>
        <p>ユーザー名: {formData.username}</p>
        <button onClick={handleReset}>別のユーザーを登録</button>
      </div>
    )
  }
  
  return (
    <form onSubmit={handleSubmit} className="complex-form">
      <h2>ユーザー登録</h2>
      
      <div className="form-group">
        <label htmlFor="username">ユーザー名</label>
        <input
          id="username"
          type="text"
          name="username"
          value={formData.username}
          onChange={handleInputChange}
          placeholder="ユーザー名を入力"
          disabled={isLoading}
        />
        {errors.username && <span className="error">{errors.username}</span>}
      </div>
      
      <div className="form-group">
        <label htmlFor="email">メールアドレス</label>
        <input
          id="email"
          type="email"
          name="email"
          value={formData.email}
          onChange={handleInputChange}
          placeholder="メールアドレスを入力"
          disabled={isLoading}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      
      <div className="form-group">
        <label htmlFor="password">パスワード</label>
        <input
          id="password"
          type="password"
          name="password"
          value={formData.password}
          onChange={handleInputChange}
          placeholder="パスワードを入力(8文字以上)"
          disabled={isLoading}
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>
      
      <div className="form-group">
        <label htmlFor="confirmPassword">パスワード(確認)</label>
        <input
          id="confirmPassword"
          type="password"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleInputChange}
          placeholder="パスワードを再度入力"
          disabled={isLoading}
        />
        {errors.confirmPassword && <span className="error">{errors.confirmPassword}</span>}
      </div>
      
      <div className="form-group">
        <label htmlFor="country"></label>
        <select
          id="country"
          name="country"
          value={formData.country}
          onChange={handleInputChange}
          disabled={isLoading}
        >
          <option value="">選択してください</option>
          <option value="japan">日本</option>
          <option value="usa">アメリカ</option>
          <option value="uk">イギリス</option>
        </select>
      </div>
      
      <div className="form-group checkbox">
        <input
          id="agreeTerms"
          type="checkbox"
          name="agreeTerms"
          checked={formData.agreeTerms}
          onChange={handleInputChange}
          disabled={isLoading}
        />
        <label htmlFor="agreeTerms">利用規約に同意します</label>
        {errors.agreeTerms && <span className="error">{errors.agreeTerms}</span>}
      </div>
      
      <div className="form-buttons">
        <button type="submit" disabled={isLoading}>
          {isLoading ? '登録中...' : '登録'}
        </button>
        <button type="button" onClick={handleReset} disabled={isLoading}>
          リセット
        </button>
      </div>
    </form>
  )
}

export default ComplexForm

実装のポイント:

  • 単一の入力ハンドラ: name属性を活用して全フィールドを一つの関数で処理
  • リアルタイムエラークリア: 入力開始時にエラーメッセージを消去
  • 包括的バリデーション: 送信前に全フィールドを検証
  • ローディング状態: 送信中はボタンとフィールドを無効化
  • UX配慮: エラーメッセージの表示、送信中の視覚的フィードバック

TypeScript版:

TSX
import { useState, type JSX } from 'react'

interface FormData {
  username: string
  email: string
  password: string
  confirmPassword: string
  agreeTerms: boolean
  country: string
}

interface FormErrors {
  username?: string
  email?: string
  password?: string
  confirmPassword?: string
  agreeTerms?: string
}

function ComplexForm(): JSX.Element {
  const [formData, setFormData] = useState<FormData>({
    username: '',
    email: '',
    password: '',
    confirmPassword: '',
    agreeTerms: false,
    country: ''
  })
  
  const [errors, setErrors] = useState<FormErrors>({})
  const [submitted, setSubmitted] = useState<boolean>(false)
  const [isLoading, setIsLoading] = useState<boolean>(false)
  
  const handleInputChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
  ): void => {
    const { name, value, type } = e.target as HTMLInputElement | HTMLSelectElement
    const checked = (e.target as HTMLInputElement).checked
    
    setFormData({
      ...formData,
      [name]: type === 'checkbox' ? checked : value
    })
    
    if (errors[name as keyof FormErrors]) {
      setErrors({
        ...errors,
        [name]: ''
      })
    }
  }
  
  const validateForm = (): FormErrors => {
    const newErrors: FormErrors = {}
    
    if (!formData.username.trim()) {
      newErrors.username = 'ユーザー名は必須です'
    }
    
    if (!formData.email.includes('@')) {
      newErrors.email = '有効なメールアドレスを入力してください'
    }
    
    if (formData.password.length < 8) {
      newErrors.password = 'パスワードは8文字以上である必要があります'
    }
    
    if (formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'パスワードが一致しません'
    }
    
    if (!formData.agreeTerms) {
      newErrors.agreeTerms = '利用規約に同意してください'
    }
    
    return newErrors
  }
  
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
    e.preventDefault()
    
    const newErrors = validateForm()
    setErrors(newErrors)
    
    if (Object.keys(newErrors).length === 0) {
      setIsLoading(true)
      try {
        await new Promise(resolve => setTimeout(resolve, 1000))
        setSubmitted(true)
        console.log('フォーム送信成功:', formData)
      } catch (error) {
        console.error('送信エラー:', error)
      } finally {
        setIsLoading(false)
      }
    }
  }
  
  const handleReset = (): void => {
    setFormData({
      username: '',
      email: '',
      password: '',
      confirmPassword: '',
      agreeTerms: false,
      country: ''
    })
    setErrors({})
    setSubmitted(false)
  }
  
  if (submitted) {
    return (
      <div className="success-message">
        <h2>登録が完了しました!</h2>
        <p>ユーザー名: {formData.username}</p>
        <button onClick={handleReset}>別のユーザーを登録</button>
      </div>
    )
  }
  
  return (
    <form onSubmit={handleSubmit} className="complex-form">
      <h2>ユーザー登録</h2>
      
      <div className="form-group">
        <label htmlFor="username">ユーザー名</label>
        <input
          id="username"
          type="text"
          name="username"
          value={formData.username}
          onChange={handleInputChange}
          placeholder="ユーザー名を入力"
          disabled={isLoading}
        />
        {errors.username && <span className="error">{errors.username}</span>}
      </div>
      
      <div className="form-group">
        <label htmlFor="email">メールアドレス</label>
        <input
          id="email"
          type="email"
          name="email"
          value={formData.email}
          onChange={handleInputChange}
          placeholder="メールアドレスを入力"
          disabled={isLoading}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>
      
      <div className="form-group">
        <label htmlFor="password">パスワード</label>
        <input
          id="password"
          type="password"
          name="password"
          value={formData.password}
          onChange={handleInputChange}
          placeholder="パスワードを入力(8文字以上)"
          disabled={isLoading}
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>
      
      <div className="form-group">
        <label htmlFor="confirmPassword">パスワード(確認)</label>
        <input
          id="confirmPassword"
          type="password"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleInputChange}
          placeholder="パスワードを再度入力"
          disabled={isLoading}
        />
        {errors.confirmPassword && <span className="error">{errors.confirmPassword}</span>}
      </div>
      
      <div className="form-group">
        <label htmlFor="country"></label>
        <select
          id="country"
          name="country"
          value={formData.country}
          onChange={handleInputChange}
          disabled={isLoading}
        >
          <option value="">選択してください</option>
          <option value="japan">日本</option>
          <option value="usa">アメリカ</option>
          <option value="uk">イギリス</option>
        </select>
      </div>
      
      <div className="form-group checkbox">
        <input
          id="agreeTerms"
          type="checkbox"
          name="agreeTerms"
          checked={formData.agreeTerms}
          onChange={handleInputChange}
          disabled={isLoading}
        />
        <label htmlFor="agreeTerms">利用規約に同意します</label>
        {errors.agreeTerms && <span className="error">{errors.agreeTerms}</span>}
      </div>
      
      <div className="form-buttons">
        <button type="submit" disabled={isLoading}>
          {isLoading ? '登録中...' : '登録'}
        </button>
        <button type="button" onClick={handleReset} disabled={isLoading}>
          リセット
        </button>
      </div>
    </form>
  )
}

export default ComplexForm

まとめ

この記事では、Reactのイベント処理を詳しく学びました。

重要なポイント:

  • Reactのイベント属性はキャメルケースで書く
  • イベントハンドラは関数を渡す(関数呼び出しではない)
  • e.preventDefault()でデフォルト動作をキャンセル
  • e.stopPropagation()でイベント伝播を停止
  • useStateとイベント処理を組み合わせてインタラクティブなUIを作成
  • TypeScriptでイベントオブジェクトの型を正確に指定

ベストプラクティス:

  • 意味のある関数名をつける
  • イベントハンドラをコンポーネント外で定義
  • 必要な情報だけをイベントオブジェクトから抽出
  • 複雑なイベント処理はカスタムフックに切り出す
  • デバウンスやスロットルでパフォーマンスを最適化

次のステップ: 次回は、条件分岐とリスト表示のテクニックについて学びます。useStateとイベント処理を組み合わせて、より複雑で実用的なUIパターンをマスターしていきましょう!

イベント処理はReactの根幹です。様々なイベント処理パターンを実装しながら、経験を積んでいってください!