Reactの基礎 - イベントハンドリング
概要
Reactにおけるイベントハンドリングは、ユーザー操作 (クリック、入力、フォーム送信等) に応じてUIを動的に更新するための仕組みである。
HTMLのDOM APIが提供するネイティブイベントとは異なり、Reactは合成イベント (SyntheticEvent) という独自のイベントシステムを採用している。
合成イベントはブラウザネイティブイベントのラッパーであり、全てのブラウザで統一されたAPIを提供することでクロスブラウザ互換性を保証する。
React 16以前ではイベントプーリング (Event Pooling) という最適化が適用されていたが、React 17以降ではこの仕組みが廃止された。
そのため、e.persist() の呼び出しは不要となり、非同期処理内でもイベントオブジェクトに安全にアクセスできる。
TypeScriptを使用したReact開発では、イベントハンドラに型情報を付与することでコンパイル時のエラー検出が可能になる。
React.MouseEvent<HTMLButtonElement> や React.ChangeEvent<HTMLInputElement> のように、イベントの種類と対象要素の型を組み合わせて指定するのが基本的なパターンである。
また、ReactのイベントはHTMLの属性名と対応しており、onclick は onClick、onchange は onChange というキャメルケースの形式で記述する。
イベントハンドラには関数そのものを渡す必要があり、onClick={handleClick} のように記述する。
onClick={handleClick()} のように関数呼び出しの形式で記述すると、レンダリング時に即座に実行されてしまうため注意が必要である。
合成イベント (SyntheticEvent)
SyntheticEventの仕組み
合成イベント (SyntheticEvent) は、Webブラウザのネイティブイベントを標準化したラッパーオブジェクトである。
Reactは異なるブラウザ間でイベントの挙動が統一されるよう、W3C仕様に準拠した形で合成イベントを実装している。
これにより、Chrome、Firefox、Safari、Edge等のWebブラウザで同一のイベント処理コードが動作する。
合成イベントは、ネイティブイベントと同等のインターフェースを持つ。
nativeEvent プロパティを通じて元のブラウザネイティブイベントオブジェクトにアクセスすることも可能である。
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
// 合成イベントオブジェクトへのアクセス
console.log(e.type); // "click"
console.log(e.target); // クリックされた要素
console.log(e.currentTarget); // ハンドラが設定されている要素
// ネイティブイベントへのアクセス
console.log(e.nativeEvent); // MouseEvent (ブラウザネイティブ)
}
React 16以前では、合成イベントオブジェクトはイベントプール (Event Pool) から再利用されていた。
そのため、非同期処理内でイベントオブジェクトにアクセスすると、既にプールに返却されて値がクリアされるという問題があった。
React 17以降では、イベントプーリングが廃止され、各イベントごとに新しいオブジェクトが割り当てられるようになった。
// React 17以降 : e.persist() の呼び出しは不要
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// 非同期処理内でも安全にアクセスできる
setTimeout(() => {
console.log(e.target.value); // React 17以降は正常に動作する
}, 1000);
}
SyntheticEventの主要な型
下表に、Reactが提供する合成イベントの主要な型を示す。
| 型名 | 対応するイベント | 主な対象要素 |
|---|---|---|
React.MouseEvent<T> |
onClick, onDoubleClick, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onMouseMove |
button, div, a等 |
React.ChangeEvent<T> |
onChange | input, select, textarea |
React.FormEvent<T> |
onSubmit, onReset, onChange | form |
React.KeyboardEvent<T> |
onKeyDown, onKeyUp, onKeyPress | input, textarea等 |
React.FocusEvent<T> |
onFocus, onBlur | input, button等 |
React.DragEvent<T> |
onDrag, onDragStart, onDragEnd, onDrop, onDragOver |
div等 |
React.ClipboardEvent<T> |
onCopy, onCut, onPaste | input, div等 |
React.TouchEvent<T> |
onTouchStart, onTouchMove, onTouchEnd | div等 |
React.PointerEvent<T> |
onPointerDown, onPointerUp, onPointerMove | div, button等 |
React.WheelEvent<T> |
onWheel | div等 |
React.AnimationEvent<T> |
onAnimationStart, onAnimationEnd, onAnimationIteration | div等 |
React.TransitionEvent<T> |
onTransitionEnd | div等 |
型パラメータ T には、イベントの対象となるHTML要素の型を指定する。
例えば、React.MouseEvent<HTMLButtonElement> はボタン要素に対するマウスイベントを表す。
イベントハンドラの基本
onClickの使用方法
onClick は最もよく使用されるイベントハンドラであり、ユーザが要素を押下した時に発生する。
import { useState } from 'react';
function LikeButton() {
const [liked, setLiked] = useState(false);
// React.MouseEvent<HTMLButtonElement> 型を指定する
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log('クリックされた位置:', e.clientX, e.clientY);
setLiked(!liked);
}
return (
<button onClick={handleClick}>
{liked ? 'いいね済み' : 'いいね'}
</button>
);
}
onChangeの使用方法
onChange は、input要素、select要素、textarea要素の値が変更された時に発生する。
import { useState } from 'react';
function NameInput() {
const [name, setName] = useState('');
// React.ChangeEvent<HTMLInputElement> 型を指定する
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setName(e.target.value);
}
return (
<div>
<input
type="text"
value={name}
onChange={handleChange}
placeholder="名前を入力"
/>
<p>入力値: {name}</p>
</div>
);
}
select 要素の場合は、React.ChangeEvent<HTMLSelectElement>、
textarea 要素の場合は、React.ChangeEvent<HTMLTextAreaElement> を使用する。
その他の主要イベント
下表に、Reactで頻繁に使用される主要なイベントハンドラを示す。
| イベントハンドラ | 発生タイミング | 対応するSyntheticEvent型 |
|---|---|---|
onSubmit |
フォームが送信された時 | React.FormEvent<HTMLFormElement>
|
onKeyDown |
キーが押された瞬間 | React.KeyboardEvent<T>
|
onKeyUp |
キーが離された瞬間 | React.KeyboardEvent<T>
|
onFocus |
要素がフォーカスを得た時 | React.FocusEvent<T>
|
onBlur |
要素がフォーカスを失った時 | React.FocusEvent<T>
|
onMouseEnter |
マウスが要素に入った時 (バブリングなし) | React.MouseEvent<T>
|
onMouseLeave |
マウスが要素から出た時 (バブリングなし) | React.MouseEvent<T>
|
onDoubleClick |
要素がダブルクリックされた時 | React.MouseEvent<T>
|
onScroll |
要素がスクロールされた時 | React.UIEvent<T>
|
onDrop |
ドラッグされた要素がドロップされた時 | React.DragEvent<T>
|
onPaste |
テキストが貼り付けられた時 | React.ClipboardEvent<T>
|
イベントハンドラの記述パターン
インライン関数
インライン関数は、JSX内にアロー関数を直接記述するパターンである。
簡潔な処理に適しており、ソースコードが短く可読性が高い。
ただし、コンポーネントが再レンダリングされるたびに新しい関数オブジェクトが生成されるため、パフォーマンスが重要な場面では注意が必要である。
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
{/* インライン関数: シンプルな処理に適する */}
<button onClick={() => setCount(count + 1)}>
増加
</button>
<button onClick={() => setCount(0)}>
リセット
</button>
<p>カウント: {count}</p>
</div>
);
}
別関数として定義
コンポーネント内に関数を定義し、ハンドラ名を属性値として渡すパターンである。
処理が複数行にわたる場合や、同じ関数を複数の要素で共有する場合に適している。
関数名に handle プレフィックスを付ける命名規則が一般的である。
function SignupForm() {
const [email, setEmail] = useState('');
// function宣言による定義
function handleEmailChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
// 複数行の処理が記述しやすい
setEmail(value.toLowerCase().trim());
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
console.log('送信:', email);
}
return (
<form onSubmit={handleSubmit}>
{/* 関数名を渡す (呼び出しではない) */}
<input
type="email"
value={email}
onChange={handleEmailChange}
/>
<button type="submit">登録</button>
</form>
);
}
アロー関数による定義
const handleClick = () => {} の形式でコンポーネント内に関数を定義するパターンである。
function宣言と機能的には同等であるが、アロー関数では this のバインディングが発生しないため、クラスコンポーネントにおいて有用である。
関数コンポーネントでは、どちらの記法を使用しても同等に機能する。
function DeleteButton({ id }: { id: string }) {
// アロー関数による定義
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
console.log('削除:', id);
};
return (
<button onClick={handleClick} className="btn-danger">
削除
</button>
);
}
パフォーマンスが重要な場面では、useCallback フックを使用して関数参照を安定化させることができる。
import { useState, useCallback } from 'react';
function OptimizedList() {
const [items, setItems] = useState<string[]>([]);
// useCallbackで関数をメモ化する
const handleAddItem = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
setItems(prev => [...prev, `アイテム${prev.length + 1}`]);
}, []); // 依存配列が空のため、初回レンダリング時の関数参照を保持する
return (
<div>
<button onClick={handleAddItem}>追加</button>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
イベントハンドラへの引数の渡し方
アロー関数でラップする方法
イベントハンドラに引数を渡す簡単な方法は、JSX内でアロー関数を使用してラップすることである。
interface Item {
id: string;
name: string;
}
interface ItemListProps {
items: Item[];
onDelete: (id: string) => void;
}
function ItemList({ items, onDelete }: ItemListProps) {
// アロー関数でラップして引数を渡す
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
{/* onClick={() => onDelete(item.id) でidを渡す */}
<button onClick={() => onDelete(item.id)}>
削除
</button>
</li>
))}
</ul>
);
}
イベントオブジェクトも合わせて渡す場合は、アロー関数の引数にイベントオブジェクトを受け取る。
function handleDelete(id: string, e: React.MouseEvent<HTMLButtonElement>) {
e.stopPropagation();
console.log('削除:', id);
}
// アロー関数でラップしてidとイベントオブジェクトを渡す
<button onClick={(e) => handleDelete(item.id, e)}>削除</button>
カリー化 (関数を返す関数)
カリー化とは、引数を受け取って新しい関数を返す手法である。
イベントハンドラにカリー化を適用すると、JSX内のコードをよりシンプルに記述できる。
function TaskList() {
const [tasks, setTasks] = useState<string[]>(['タスク1', 'タスク2']);
// カリー化 : idを受け取って、イベントハンドラを返す関数
const handleDelete = (index: number) => () => {
setTasks(prev => prev.filter((_, i) => i !== index));
};
return (
<ul>
{tasks.map((task, index) => (
<li key={index}>
{task}
{/* handleDelete(index) がイベントハンドラを返す */}
<button onClick={handleDelete(index)}>
削除
</button>
</li>
))}
</ul>
);
}
下表に、2つのアプローチを比較する。
| アプローチ | 記述例 | メリット | デメリット |
|---|---|---|---|
| アロー関数でラップ | onClick={() => handleClick(id)} |
直感的でわかりやすい | 毎回新しい関数が生成される |
| カリー化 | onClick={handleClick(id)} |
JSXがシンプルになる | 関数の意図が理解しにくい場合がある |
preventDefaultとstopPropagation
preventDefault
e.preventDefault() は、Webブラウザのデフォルト動作を抑止するメソッドである。
主なユースケースとして、フォームの送信時にページリロードを防ぐ場合と、リンクのナビゲーションを抑止する場合がある。
- フォーム送信のデフォルト動作を抑止する場合
function LoginForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); function handleSubmit(e: React.FormEvent<HTMLFormElement>) { // デフォルトのページリロードを抑止する e.preventDefault(); // カスタムの送信処理を実行する console.log('ログイン:', { email, password }); } return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={e => setEmail(e.target.value)} /> <input type="password" value={password} onChange={e => setPassword(e.target.value)} /> <button type="submit">ログイン</button> </form> ); }
- リンクのデフォルトナビゲーションを抑止する場合
function CustomLink({ href, children }: { href: string; children: React.ReactNode }) { function handleClick(e: React.MouseEvent<HTMLAnchorElement>) { // デフォルトのページ遷移を抑止する e.preventDefault(); // カスタムのナビゲーション処理を実行する console.log('カスタムナビゲーション:', href); } return ( <a href={href} onClick={handleClick}> {children} </a> ); }
stopPropagation
e.stopPropagation() は、イベントが親要素に向けて伝播 (バブリング) するのを停止するメソッドである。
ネストした要素でイベントを処理する際、親要素のハンドラが意図せず実行されることを防ぐために使用する。
function Modal({ onClose, children }: { onClose: () => void; children: React.ReactNode }) {
function handleOverlayClick(e: React.MouseEvent<HTMLDivElement>) {
// オーバーレイのクリックでモーダルを閉じる
onClose();
}
function handleContentClick(e: React.MouseEvent<HTMLDivElement>) {
// コンテンツ内のクリックが親 (オーバーレイ) に伝播しないようにする
e.stopPropagation();
}
return (
<div className="overlay" onClick={handleOverlayClick}>
<div className="modal-content" onClick={handleContentClick}>
{children}
</div>
</div>
);
}
イベントの伝播 (バブリングとキャプチャ)
DOMイベントの伝播は以下の3つのフェーズで構成される。
| フェーズ | 内容 |
|---|---|
| キャプチャフェーズ | イベントがルート要素から対象要素に向かって伝播するフェーズonClickCapture 等のキャプチャフェーズハンドラが実行される。
|
| ターゲットフェーズ | イベントが対象要素に到達するフェーズ 対象要素に設定されたハンドラが実行される。 |
| バブリングフェーズ | イベントが対象要素から親要素に向かって伝播するフェーズonClick 等の通常のイベントハンドラが実行される。
|
Reactでは、onClickCapture のように Capture サフィックスを付けることで、キャプチャフェーズのハンドラを登録できる。
function EventPropagationExample() {
return (
{/* キャプチャフェーズ: 最初に実行される */}
<div onClickCapture={() => console.log('1: 外側 (キャプチャ)')}>
{/* キャプチャフェーズ: 2番目に実行される */}
<div onClickCapture={() => console.log('2: 内側 (キャプチャ)')}>
{/* ターゲットフェーズ: ボタン自体のハンドラ */}
<button
onClick={() => console.log('3: ボタン (ターゲット)')}
>
クリック
</button>
</div>
{/* バブリングフェーズ: 最後に実行される */}
{/* ※ onClickCapture と onClick は同じ要素に共存できる */}
</div>
);
// クリック時の実行順: 1 → 2 → 3 (バブリングは内側から外側の順)
}
onMouseEnter と onMouseLeave はバブリングが発生しないイベントであり、stopPropagation の影響を受けない。
TypeScriptでのイベント型定義
イベントハンドラの型
下表に、TypeScriptでReactのイベントハンドラを定義する時に使用する主要な型を示す。
| イベントハンドラ | TypeScript型 | 主な対象要素 |
|---|---|---|
| onClick, onDoubleClick | React.MouseEvent<HTMLButtonElement> |
button |
| onClick, onDoubleClick | React.MouseEvent<HTMLDivElement> |
div |
| onClick, onDoubleClick | React.MouseEvent<HTMLAnchorElement> |
a |
| onChange | React.ChangeEvent<HTMLInputElement> |
input |
| onChange | React.ChangeEvent<HTMLSelectElement> |
select |
| onChange | React.ChangeEvent<HTMLTextAreaElement> |
textarea |
| onSubmit | React.FormEvent<HTMLFormElement> |
form |
| onKeyDown, onKeyUp | React.KeyboardEvent<HTMLInputElement> |
input |
| onFocus, onBlur | React.FocusEvent<HTMLInputElement> |
input |
| onDragStart, onDrop | React.DragEvent<HTMLDivElement> |
div |
| onCopy, onPaste | React.ClipboardEvent<HTMLInputElement> |
input |
| onTouchStart, onTouchEnd | React.TouchEvent<HTMLDivElement> |
div |
| onPointerDown, onPointerUp | React.PointerEvent<HTMLDivElement> |
div |
また、イベントハンドラ関数自体の型を指定する場合は、React.EventHandler 型ファミリーを使用できる。
| 型名 | 説明 |
|---|---|
| React.MouseEventHandler<T> | (e: React.MouseEvent<T>) => void の省略形 |
| React.ChangeEventHandler<T> | (e: React.ChangeEvent<T>) => void の省略形 |
| React.FormEventHandler<T> | (e: React.FormEvent<T>) => void の省略形 |
| React.KeyboardEventHandler<T> | (e: React.KeyboardEvent<T>) => void の省略形 |
| React.FocusEventHandler<T> | (e: React.FocusEvent<T>) => void の省略形 |
カスタムイベントハンドラの型定義
コンポーネントのPropsにイベントハンドラを含める場合は、interface を使用して型を定義する。
- 基本的なコールバックパターン
interface ButtonProps { label: string; disabled?: boolean; // イベントハンドラをPropsに含める onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; } function Button({ label, disabled, onClick }: ButtonProps) { return ( <button onClick={onClick} disabled={disabled}> {label} </button> ); } // 使用例 function App() { const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => { console.log('クリック座標:', e.clientX, e.clientY); }; return ( <Button label="送信" onClick={handleButtonClick} /> ); }
- EventHandler型を使用したPropsの定義
interface InputProps { value: string; placeholder?: string; // React.ChangeEventHandler<HTMLInputElement> を使用する onChange: React.ChangeEventHandler<HTMLInputElement>; onBlur?: React.FocusEventHandler<HTMLInputElement>; onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>; } function TextInput({ value, placeholder, onChange, onBlur, onKeyDown }: InputProps) { return ( <input type="text" value={value} placeholder={placeholder} onChange={onChange} onBlur={onBlur} onKeyDown={onKeyDown} /> ); }
- カスタムデータをコールバックで返すパターン
interface SelectableItem { id: string; name: string; } interface SelectableListProps { items: SelectableItem[]; // イベントオブジェクトではなくカスタムデータを返すコールバック onSelect: (item: SelectableItem) => void; } function SelectableList({ items, onSelect }: SelectableListProps) { return ( <ul> {items.map(item => ( <li key={item.id} onClick={() => onSelect(item)} style={{ cursor: 'pointer' }} > {item.name} </li> ))} </ul> ); } // 使用例 function App() { const handleSelect = (item: SelectableItem) => { console.log('選択されたアイテム:', item.name); }; return ( <SelectableList items={[{ id: '1', name: 'アイテムA' }, { id: '2', name: 'アイテムB' }]} onSelect={handleSelect} /> ); }
関連情報
- Reactの基礎 - TSXの基本構文
- Reactの基礎 - コンポーネント
- Reactの基礎 - PropsとChildren
- Reactの基礎 - フォーム(制御コンポーネント)
- Reactの基礎 - フォーム(非制御コンポーネント)