イベント処理
イベント処理は、React アプリの神経終末のようなものです。ユーザーがボタンをクリックしたり、テキストを入力したり、フォームを送信したりすると、これらの操作はイベントを通じて処理されます。React のイベントシステムは、ネイティブの DOM イベントよりもスマートで、統一性があります。
1. 学習内容
- Reactの複合イベントとネイティブイベントの違い 一般的なイベント:onClick / onChange / onSubmit / onKeyDown
- イベントオブジェクトの処理
- イベントハンドラ関数へのパラメータの渡し方
- デフォルトの動作とイベントのバブリングを防ぐ
2. フォームの操作に関する話
(1) 課題:フォーム送信における3つの落とし穴
ボブはユーザー登録フォームを作成しており、ネイティブのJavaScriptでイベントハンドラを記述しています:
JAVASCRIPT
// Native JS:Various compatibility issues and details need to be handled manually
// 1. Get DOM Element
const form = document.getElementById('register-form')
const nameInput = document.getElementById('name')
const submitBtn = document.getElementById('submit-btn')
// 2. Event Binding
form.addEventListener('submit', function(event) {
event.preventDefault() // Prevent the page from refreshing
// 3. Manually Retrieving Form Data
const formData = new FormData(form)
const data = Object.fromEntries(formData)
// 4. Submit Data
fetch('/api/register', {
method: 'POST',
body: JSON.stringify(data)
})
})
// 5. Real-Time Input Validation
nameInput.addEventListener('input', function(event) {
if (event.target.value.length < 2) {
showError('Name (at least) 2 characters')
} else {
hideError()
}
})
// 6. Prevent duplicate submissions when the Submit button is clicked
let isSubmitting = false
submitBtn.addEventListener('click', function() {
if (isSubmitting) return
isSubmitting = true
submitBtn.disabled = true
submitBtn.textContent = 'Submitting......'
})
ボブの問題:
- コードの分散:イベントのバインディング、データ処理、UIの更新が、さまざまな場所に分散している
- メモリリーク:イベントリスナーの解除を忘れる
- 互換性の問題:ブラウザごとにイベントAPIに違いがあります
(2) Reactにおけるイベント処理
JSX
function RegisterForm() {
const [name, setName] = React.useState('')
const [isSubmitting, setIsSubmitting] = React.useState(false)
// Submit for Processing
function handleSubmit(event) {
event.preventDefault()
setIsSubmitting(true)
fetch('/api/register', {
method: 'POST',
body: JSON.stringify({ name })
}).finally(() => setIsSubmitting(false))
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Please enter your name"
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting......' : 'Register'}
</button>
</form>
)
}
メリット:コード行数が30行から15行に削減され、DOMの操作が不要となり、イベントの解除に関する問題やブラウザ間の互換性の問題も発生しなくなりました。
3. Reactの複合イベント
ReactのイベントはネイティブなDOMイベントではなく、SyntheticEventsです。Reactはトップレベルでこれらを監視し、クロスブラウザ対応のイベントシステムをシミュレートしています。
| 特集 | ネイティブDOMイベント | Reactの複合イベント |
|---|---|---|
| バインディング方法 | addEventListener / onclick |
JSXに直接記述 |
| ブラウザの互換性 | 差異の手動処理が必要 | 自動標準化 |
| メモリ管理 | リスナーは手動で削除する必要がある | 自動クリーンアップ |
| イベントオブジェクト | ネイティブイベント | SyntheticEvent |
| 気泡の発生を防ぐ | event.stopPropagation() |
同一(標準化) |
| ブロックのデフォルト | event.preventDefault() |
同一(標準化) |
graph TB
A[The user clicks the button] --> B[React Root Node Event Handling]
B --> C[Create a Synthetic Event Object]
C --> D[Simulated Bubbles/Capture Phase]
D --> E[Call JSX Bound to the middle handler]
style B fill:#61dafb,color:#000
style C fill:#1890ff,color:#fff
4. よくあるイベントのクイックリファレンス
| イベント名 | トリガー条件 | 一般的なシナリオ | イベントオブジェクトの種類 |
|---|---|---|---|
onClick |
要素がクリックされた | ボタン、リンク、カード | MouseEvent |
onChange |
入力フィールドの内容の変更 | フォームの入力欄、ドロップダウンメニュー | ChangeEvent |
onSubmit |
フォームを送信 | ログイン/登録/検索 | FormEvent |
onFocus |
要素にフォーカスが当たる | 入力フィールドがハイライトされる | FocusEvent |
onBlur |
要素のフォーカスが外れる | 入力の検証 | FocusEvent |
onKeyDown |
キーを押してください | ショートカットキー、Enterキーで検索 | KeyboardEvent |
onKeyUp |
プレスリリース | リアルタイム検索 | KeyboardEvent |
onMouseEnter |
マウスオーバー | ホバー効果 | MouseEvent |
onMouseLeave |
マウスアウト | ホバー効果 | MouseEvent |
onScroll |
スクロール | 無限スクロール | UIEvent |
(1) ▶ サンプル:よくあるイベントの包括的な概要
JSX
// ============================================
// Example:Complete Event Handling for a Login Form
// ============================================
function LoginForm() {
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [errors, setErrors] = React.useState({})
const [focusedField, setFocusedField] = React.useState('')
function handleSubmit(event) {
event.preventDefault()
// Form Validation
const newErrors = {}
if (!email.includes('@')) newErrors.email = 'The email address format is incorrect.'
if (password.length < 6) newErrors.password = 'Password must be at least 6 chars'
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
return
}
// Submit Login
console.log('Log In:', { email, password })
}
function handleKeyDown(event) {
// Press Escape key to clear focus
if (event.key === 'Escape') {
event.target.blur()
}
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto' }}>
<h2>Log In</h2>
{/* Enter your email address */}
<div style={{ marginBottom: '16px' }}>
<label>Email:</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
onFocus={() => setFocusedField('email')}
onBlur={() => { setFocusedField(''); setErrors({...errors, email: undefined}) }}
onKeyDown={handleKeyDown}
style={{
width: '100%',
padding: '8px',
borderColor: errors.email ? '#ff4d4f' : focusedField === 'email' ? '#1890ff' : '#d9d9d9'
}}
/>
{errors.email && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.email}</p>}
</div>
{/* Password Entry */}
<div style={{ marginBottom: '16px' }}>
<label>Password:</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
onFocus={() => setFocusedField('password')}
onBlur={() => setFocusedField('')}
onKeyDown={handleKeyDown}
style={{
width: '100%',
padding: '8px',
borderColor: errors.password ? '#ff4d4f' : focusedField === 'password' ? '#1890ff' : '#d9d9d9'
}}
/>
{errors.password && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.password}</p>}
</div>
{/* Submit Button */}
<button
type="submit"
onClick={() => console.log('Clicked the "Log In" button')}
onMouseEnter={() => console.log('Button on Mouse Hover')}
onMouseLeave={() => console.log('Mouse leaves button')}
style={{
width: '100%',
padding: '10px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Log In
</button>
</form>
)
}
5. イベントパラメータの受け渡し
(1) パラメータの直接渡し
それを矢印関数で囲み、追加のパラメータを直接渡します:
JSX
function UserList({ users, onDelete }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
{/* Arrow Functions:Incoming user.id As an additional parameter */}
<button onClick={() => onDelete(user.id)}>
Delete
</button>
</li>
))}
</ul>
)
}
// Usage
<UserList
users={users}
onDelete={(id) => console.log('Delete User:', id)}
/>
(2) イベントオブジェクトとカスタムパラメータの両方を取得する
JSX
function ColorPicker({ colors, onSelect }) {
return (
<div>
<p>Select a Color:</p>
{colors.map(color => (
<button
key={color}
onClick={(event) => {
// event:Native Synthesis Event Object
// color:Custom Parameters
onSelect(color)
console.log('Click here:', event.clientX, event.clientY)
}}
style={{
backgroundColor: color,
width: '40px',
height: '40px',
border: '2px solid #ddd',
borderRadius: '50%',
margin: '4px',
cursor: 'pointer'
}}
/>
))}
</div>
)
}
(3) イベントオブジェクトを渡す3つの方法
| メソッド | 構文 | 適用可能なシナリオ |
|---|---|---|
| 暗黙の引数渡し | onClick={handleClick} |
追加のパラメータは不要 |
| 矢印関数 | onClick={() => handleClick(id)} |
パラメータが必要 |
| 同時に通過 | onClick={(e) => handleClick(e, id)} |
イベントオブジェクトとカスタムパラメータが必要 |
6. デフォルト動作とバブリングの防止
(1) デフォルトの動作を防ぐ:preventDefault()
最も一般的な使用例は、フォームが送信された際にページが再読み込みされないようにすることです:
JSX
function SearchForm() {
const [query, setQuery] = React.useState('')
function handleSubmit(event) {
event.preventDefault() // Prevent the page from refreshing
// Execute Custom Search Logic
console.log('Search:', query)
}
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit">Search</button>
</form>
)
}
// Other scenarios where the default behavior needs to be overridden:
// - Link Redirection:<a href="#" onClick={e => e.preventDefault()}>
// - Context Menu:onContextMenu={e => e.preventDefault()}
// - Drag-and-Drop File Upload:onDragOver={e => e.preventDefault()}
(2) 気泡の発生を防ぐ:stopPropagation()
JSX
function Modal() {
return (
// Click the mask layer to close the pop-up window
<div
style={overlayStyle}
onClick={() => console.log('Click the mask layer → Close Pop-up')}
>
{/* Clicking on the pop-up content does not display a speech bubble */}
<div
style={modalStyle}
onClick={(event) => {
event.stopPropagation() // Prevent Bubbles from Rising to the Mask Layer
console.log('Click on the pop-up window content')
}}
>
<h2>Pop-up Title</h2>
<p>Pop-up Content</p>
<button onClick={() => console.log('Close')}>Close</button>
</div>
</div>
)
}
// Click "Modal Content" area → only triggers modal onClick
// Click "Overlay" (outside modal) → trigger mask layer onClick
7. 完全な例:To-Doリスト(包括的なイベント処理)
JSX
// ============================================
// Example:Todo List(Comprehensive Incident Handling)
// Features:Add a Task、Marked as complete、Delete Task、Double-click to edit
// ============================================
function TodoApp() {
const [todos, setTodos] = React.useState([
{ id: 1, text: 'Study React Event', done: false },
{ id: 2, text: 'Complete the homework', done: false }
])
const [input, setInput] = React.useState('')
// 1. Add a Task(onSubmit + Enter key)
function handleSubmit(event) {
event.preventDefault()
if (!input.trim()) return
setTodos([...todos, {
id: Date.now(),
text: input.trim(),
done: false
}])
setInput('')
}
// 2. Quick Add with the Enter Key(onKeyDown)
function handleKeyDown(event) {
if (event.key === 'Enter' && input.trim()) {
handleSubmit(event)
}
}
// 3. Switch to "Completed" status(onChange)
function handleToggle(id) {
setTodos(todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
))
}
// 4. Delete Task(onClick + Parameter Passing)
function handleDelete(id, event) {
event.stopPropagation() // Prevent triggering li the incident
setTodos(todos.filter(t => t.id !== id))
}
// 5. Double-click to edit(onDoubleClick)
function handleEdit(todo) {
const newText = prompt('Edit Task:', todo.text)
if (newText && newText.trim()) {
setTodos(todos.map(t =>
t.id === todo.id ? { ...t, text: newText.trim() } : t
))
}
}
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>📋 Todo List</h2>
{/* Input Form */}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Input Task,Press Enter to add..."
style={{
width: '70%', padding: '8px',
border: '1px solid #d9d9d9', borderRadius: '4px'
}}
/>
<button type="submit" style={{
padding: '8px 16px', marginLeft: '8px',
backgroundColor: '#1890ff', color: 'white',
border: 'none', borderRadius: '4px', cursor: 'pointer'
}}>
Add
</button>
</form>
{/* Statistics */}
<p style={{ color: '#666', fontSize: '14px' }}>
Total {todos.length} items, Completed {todos.filter(t => t.done).length} items
</p>
{/* Task List */}
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li
key={todo.id}
onDoubleClick={() => handleEdit(todo)}
style={{
display: 'flex', alignItems: 'center',
padding: '10px', margin: '4px 0',
backgroundColor: todo.done ? '#f6ffed' : '#fff',
border: '1px solid #f0f0f0',
borderRadius: '4px',
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#999' : '#333',
cursor: 'pointer'
}}
>
{/* Checkbox */}
<input
type="checkbox"
checked={todo.done}
onChange={() => handleToggle(todo.id)}
style={{ marginRight: '10px' }}
/>
{/* Mission Text */}
<span style={{ flex: 1 }}>{todo.text}</span>
{/* Delete Button */}
<button
onClick={(e) => handleDelete(todo.id, e)}
style={{
padding: '2px 8px',
backgroundColor: 'transparent',
color: '#ff4d4f',
border: 'none',
cursor: 'pointer',
fontSize: '16px'
}}
>
✕
</button>
</li>
))}
</ul>
</div>
)
}
インタラクションの流れ:
- テキストボックスにタスク名を入力し、Enter キーを押すか、「追加」ボタンをクリックします → タスクがリストに追加されます
- チェックボックスにチェックを入れる → タスクを完了としてマークする(テキストに打ち消し線が入り、灰色で表示される)
- ✕ ボタンをクリック → タスクを削除する(ダブルクリックで編集機能が起動しないようにするため、
stopPropagation)- 任意のタスクをダブルクリック → 編集用のダイアログボックスが表示されます
(1) ▶ サンプル:キーボードショートカットとフォーカスの管理
JSX
// ============================================
// Example:Shortcut Keys Panel——Integrated Use of Keyboard Events and Focus Management
// Features:Usage onKeyDown Implement Keyboard Shortcuts,Usage onFocus/onBlur Management Focus
// ============================================
function ShortcutPanel() {
const [output, setOutput] = React.useState('')
const [activeKeys, setActiveKeys] = React.useState(new Set())
const inputRef = React.useRef(null)
// Keyboard Event Handling
function handleKeyDown(event) {
const { key, ctrlKey, shiftKey, altKey } = event
// Ctrl+S:Save
if (ctrlKey && key === 's') {
event.preventDefault()
setOutput('💾 Saved(Ctrl+S)')
}
// Ctrl+Z:Revoke
else if (ctrlKey && key === 'z') {
event.preventDefault()
setOutput('↩️ Undo(Ctrl+Z)')
}
// Escape:Clear Output
else if (key === 'Escape') {
setOutput('')
event.target.blur()
}
// Enter:Confirm
else if (key === 'Enter') {
setOutput(`✅ Confirm:${event.target.value || '(Empty input)'}`)
}
// Display the currently pressed key
setActiveKeys(prev => new Set([...prev, key]))
}
function handleKeyUp(event) {
setActiveKeys(prev => {
const next = new Set(prev)
next.delete(event.key)
return next
})
}
// Autofocus
React.useEffect(() => {
inputRef.current.focus()
}, [])
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>⌨️ Shortcut Keys Panel</h2>
<input
ref={inputRef}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onFocus={() => setOutput('The input field is currently focused')}
onBlur={() => setOutput('The input field has lost focus')}
placeholder="Type here,Try the shortcut keys..."
style={{
width: '100%',
padding: '10px',
fontSize: '16px',
border: '2px solid #1890ff',
borderRadius: '6px',
outline: 'none'
}}
/>
{/* Keyboard Shortcut Tips */}
<div style={{ marginTop: '12px', display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{[
{ key: 'Ctrl+S', desc: 'Save' },
{ key: 'Ctrl+Z', desc: 'Revoke' },
{ key: 'Enter', desc: 'Confirm' },
{ key: 'Escape', desc: 'Clear/blur' }
].map(shortcut => (
<span key={shortcut.key} style={{
padding: '4px 10px',
backgroundColor: '#f5f5f5',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '12px',
fontFamily: 'monospace'
}}>
{shortcut.key} <span style={{ color: '#999' }}>{shortcut.desc}</span>
</span>
))}
</div>
{/* Currently Pressed Key */}
{activeKeys.size > 0 && (
<p style={{ marginTop: '8px', fontSize: '13px', color: '#666' }}>
Currently Pressed Key:{[...activeKeys].join(' + ')}
</p>
)}
{/* Operation Output */}
{output && (
<div style={{
marginTop: '12px',
padding: '10px',
backgroundColor: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '4px',
color: '#52c41a',
fontWeight: 'bold'
}}>
{output}
</div>
)}
</div>
)
}
(2) ▶ サンプル:ドラッグ&ドロップによる並べ替えイベントの処理
JSX
function DragSortList() {
const [items, setItems] = useState(['Apple', 'Banana', 'Cherry', 'Date'])
const [dragIdx, setDragIdx] = useState(null)
function handleDragStart(idx) { setDragIdx(idx) }
function handleDragOver(e, idx) {
e.preventDefault()
if (dragIdx === null || dragIdx === idx) return
setItems(prev => {
const updated = [...prev]
const [moved] = updated.splice(dragIdx, 1)
updated.splice(idx, 0, moved)
return updated
})
setDragIdx(idx)
}
function handleDragEnd() { setDragIdx(null) }
return (
<div style={{ maxWidth: 300, margin: '0 auto' }}>
<h3>Drag to Reorder</h3>
{items.map((item, idx) => (
<div key={item} draggable
onDragStart={() => handleDragStart(idx)}
onDragOver={e => handleDragOver(e, idx)}
onDragEnd={handleDragEnd}
style={{
padding: '8px 12px', marginBottom: 4, borderRadius: 4, cursor: 'grab',
background: dragIdx === idx ? '#e6f7ff' : '#f5f5f5',
border: dragIdx === idx ? '2px solid #1890ff' : '2px solid transparent',
}}>
{item}
</div>
))}
</div>
)
}
(3) ▶ サンプル:イベントリスナーをラップするためのカスタムフック
JSX
function useEventListener(event, handler, element = window) {
useEffect(() => {
element.addEventListener(event, handler)
return () => element.removeEventListener(event, handler)
}, [event, handler, element])
}
function MouseTracker() {
const [pos, setPos] = useState({ x: 0, y: 0 })
const handler = useCallback(e => setPos({ x: e.clientX, y: e.clientY }), [])
useEventListener('mousemove', handler)
return (
<div style={{ padding: 20 }}>
<p>Mouse: ({pos.x}, {pos.y})</p>
<div style={{
width: 200, height: 200, border: '1px solid #ddd', position: 'relative', borderRadius: 4,
}}>
<div style={{
width: 10, height: 10, borderRadius: '50%', background: '#1890ff',
position: 'absolute', left: Math.min(pos.x - 100, 190), top: Math.min(pos.y - 100, 190),
transition: 'left 0.1s, top 0.1s',
}} />
</div>
</div>
)
}
(4) ▶ サンプル:フォーム送信とイベントの組み合わせ
JSX
function SearchForm({ onSearch }) {
const [query, setQuery] = useState('')
const [category, setCategory] = useState('all')
function handleSubmit(e) {
e.preventDefault()
onSearch({ query, category })
}
function handleReset() {
setQuery('')
setCategory('all')
}
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
<input value={query} onChange={e => setQuery(e.target.value)}
placeholder="Search..." style={{ flex: 1, padding: 8, borderRadius: 4 }} />
<select value={category} onChange={e => setCategory(e.target.value)}
style={{ padding: 8, borderRadius: 4 }}>
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button type="submit" style={{ padding: '6px 16px', cursor: 'pointer' }}>Search</button>
<button type="button" onClick={handleReset} style={{ padding: '6px 16px', cursor: 'pointer' }}>Reset</button>
</div>
</form>
)
}
❓ よくある質問
Q なぜ単に
handleClick(id) ではなく、矢印関数 () => handleClick(id) を使うのですか?A
onClick={handleClick(id)} を直接記述すると、React はクリックされるまで待つのではなく、レンダリング中に handleClick(id) を即座に実行してしまいます。これは、(括弧付きの)関数呼び出しであり、(括弧なしの)関数参照ではないためです。正しい方法は、onClick={() => handleClick(id)}(実行を遅延させる矢印関数)または onClick={handleClick}(引数が渡されない場合に関数名を直接参照する)です。Q 1つの要素に複数の同一のイベントをバインドすることはできますか?
A 2つの
onClickを直接バインドすることはできません。解決策:① 同じハンドラー内で複数の関数onClick={() => { fn1(); fn2() }}を呼び出す;② または、その要素を複数のHOCでラップする。通常、要素に必要なイベントハンドラーは1つだけであり、その中に複数のロジックを呼び出すことができます。Q Reactのシンセティックイベントとネイティブイベントの違いは何ですか?
A Reactの
SyntheticEventは、ネイティブイベントをラップしたクロスブラウザ対応のラッパーであり、統一されたAPI(e.preventDefault()やe.stopPropagation()など)を提供し、すべてのブラウザで一貫した動作を実現します。合成イベントはイベントデリゲーションの仕組みに基づいて動作します。つまり、すべてのイベントは個々のDOM要素ではなく、ルートノードにアタッチされます。React 17以降では、イベントはdocumentではなくルートノードにデリゲートされるため、複数のバージョンのReactが共存する場合の競合を防ぐことができます。📖 まとめ
- React は SyntheticEvent を使用しており、これによりブラウザ間の互換性と自動クリーンアップが保証されます
- 一般的なイベント:onClick、onChange、onSubmit、onKeyDown、onFocus/onBlur
- 追加のパラメータを渡すには、矢印関数を使用する
onClick={() => handler(id)} event.preventDefault()デフォルトの挙動を防止する、event.stopPropagation()バブリングを防止するonClick={handler(id)}と入力しないでください(すぐに実行されてしまいます)。代わりにonClick={() => handler(id)}を使用してください。
📝 練習問題
- 基本演習(難易度 ⭐):ボタンがクリックされるたびに、表示されているカウントを1ずつ増やす
ButtonCounterコンポーネントを作成してください。onClickイベントを使用してください。 - 上級演習(難易度 ⭐⭐):「デバウンス検索」を実装した
SearchInputコンポーネントを作成してください。この検索機能は、ユーザーが入力を停止してから 500 ms 後に自動的にトリガーされます。onChangeおよびuseEffectを使用してください。 - 課題(難易度:⭐⭐⭐):ドラッグ&ドロップによる並べ替え機能を実装した
DragAndDropListコンポーネントを作成してください。onDragStart、onDragOver、およびonDropイベントを使用してください。リストの項目を新しい位置にドラッグして移動できるようにしてください。



