イベントハンドリング
React の合成イベントシステムとイベントハンドラの書き方、引数の渡し方、よくある落とし穴を見ていきます。19 章 イベントの型付けと 27 章 Server Actions の土台です。
5 章で state と useState を学びながら、自然と onClick というイベントハンドラを使いました。それ自体でも動きはしましたが、React のイベント処理方式には知っておくべきことがいくつかあります。本章で本格的に扱います。
本章で押さえるイベントモデルは 19 章(イベントとフォームの型付け)で TypeScript によって固められ、27 章(Server Actions とフォーム)で新しいモデル(<form action={fn}>)へもう一度拡張されます。本章の基本パターンをしっかり押さえておけば、その後の章が軽く読めます。
React でイベントを処理する方法 #
React ではイベントを JSX 属性としてハンドラ関数を渡す方式で処理します。HTML の onclick ではなく、camelCase の onClick を使うという点だけ違います。
function App() {
function handleClick() {
alert('ボタンがクリックされました!');
}
return <button onClick={handleClick}>クリック</button>;
}
export default App;ここでよくある間違いが 1 つあります。関数を呼び出して(handleClick())はならず、関数自体(handleClick)を渡す必要があります。
<button onClick={handleClick()}>クリック</button>こう書くとコンポーネントがレンダリングされた瞬間に handleClick() が実行され、alert が出てしまいます。さらに onClick には handleClick の返り値(undefined)が登録されるので、本物のクリックには何も起きません。
<button onClick={handleClick}>クリック</button>関数の参照だけを渡して、呼び出しは React がクリック時点に行います。この違いを必ず覚えておいてください。
インラインハンドラ #
簡単なハンドラはいっそ JSX の中にアロー関数で直接書くこともあります。
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(prev => prev + 1)}>
カウント: {count}
</button>
);
}() => setCount(prev => prev + 1) はクリック時点に呼ばれる無名関数です。1 行で済む単純なハンドラはインラインに置く方が楽で、ロジックが長くなったら別関数に切り出す方が読みやすくなります。決まったルールはなく、チームや本人の好みに従います。
関数に引数を渡す #
ハンドラ関数に引数を渡す必要があるときは、インラインのアロー関数で包む必要があります。
function App() {
function handleClick(name) {
alert(`${name}さん、こんにちは!`);
}
return (
<>
<button onClick={() => handleClick('太郎')}>太郎に挨拶</button>
<button onClick={() => handleClick('花子')}>花子に挨拶</button>
</>
);
}次のように書いてはいけません。
<button onClick={handleClick('太郎')}>...</button>上で見たとおり、これはレンダリング即時に呼び出されてしまいます。引数を渡すには必ずアロー関数で一度包んで、「クリックされたときに呼び出してください」という意味を作る必要があります。
イベントオブジェクト #
イベントハンドラは最初の引数としてイベントオブジェクトを受け取ります。このオブジェクトには、どの要素でどんなイベントが起きたかについての情報が含まれています。
import { useState } from 'react';
function InputDemo() {
const [text, setText] = useState('');
function handleChange(e) {
setText(e.target.value);
}
return (
<div>
<input type="text" value={text} onChange={handleChange} />
<p>入力値: {text}</p>
</div>
);
}
export default InputDemo;引数名はふつう e または event を使います。e.target はイベントが発生した DOM 要素で、e.target.value で入力値を取り出せます。
React のイベントオブジェクトは、正確にはブラウザのネイティブイベントではなく合成イベント(SyntheticEvent) です。React がすべてのブラウザで同じように動くように一度包んだオブジェクトです。API はネイティブイベントとほぼ同じなので、普段は気にする必要がありません。e.preventDefault()、e.target、e.key のような馴染みのあるプロパティ / メソッドをそのまま使えます。
19 章(イベントとフォームの型付け)では、この合成イベントの正確な型 — ChangeEvent<HTMLInputElement>、FormEvent、KeyboardEvent — を扱います。また e.target と e.currentTarget の型の違いもそこで押さえます。
よく使うイベント #
最もよく使うイベントハンドラを整理すると次のとおりです。
onClick— クリックonChange— 入力要素(input、textarea、select)の値が変わるときonSubmit— フォームが送信されるときonKeyDown/onKeyUp— キーが押されたり離されたりするときonMouseEnter/onMouseLeave— マウスが要素の上に入ったり離れたりするときonFocus/onBlur— フォーカス取得 / 解除
各イベントは、それに見合った情報をイベントオブジェクトに入れて渡してくれます。onChange なら e.target.value を、onKeyDown なら e.key(押されたキーの名前)を見ます。
function SearchBox() {
function handleKeyDown(e) {
if (e.key === 'Enter') {
alert('Enter キーが押されました');
}
}
return <input type="text" onKeyDown={handleKeyDown} />;
}既定動作を防ぐ #
ブラウザはあるイベントに対して既定の動作を持っています。フォームが送信されるとページがリロードされ、リンクをクリックするとページが遷移します。この既定動作を防ぐには、イベントオブジェクトの preventDefault() を呼び出します。
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
function handleSubmit(e) {
e.preventDefault(); // フォーム送信によるページリロードを防ぐ
console.log('送信されたメール:', email);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">ログイン</button>
</form>
);
}
export default LoginForm;フォームは送信ボタンを押したり入力欄で Enter を押したりすると自動的に onSubmit が発火します。このとき e.preventDefault() がないとブラウザがページをリロードしてしまい、私たちが書いた処理ロジックの意味がなくなります。
27 章(Server Actions とフォーム)では、この preventDefault() の必要性がなくなる新しいモデルを扱います。<form action={serverFn}> の中ではページリロードが起きずにサーバ関数が実行されます。本章のパターンがそのモデルへ自然につながると頭に置いておいてください。
イベントハンドラを props として渡す #
イベントハンドラもただの関数なので、props として子コンポーネントに渡せます。このパターンは子が起こしたイベントを親が処理する必要があるとき、非常によく使われます。
function Button({ label, onClick }) {
return (
<button onClick={onClick} style={{ padding: '8px 16px' }}>
{label}
</button>
);
}
export default Button;import Button from './Button';
function App() {
function handleSave() {
alert('保存しました');
}
function handleCancel() {
alert('キャンセルしました');
}
return (
<>
<Button label="保存" onClick={handleSave} />
<Button label="キャンセル" onClick={handleCancel} />
</>
);
}親はどんな行動をするか(ハンドラ)を渡し、子はいつその行動が起きたか(クリック)を知らせる構造です。ハンドラ prop の名前は慣例として on で始まるように付けます(onClick、onSave、onItemSelect など)。
この「子のイベント → 親のハンドラ」パターンが 11 章(state のリフトアップ)の核となる道具になります。
ハンドラの中で state を変更する #
5 章で見たパターンですが、イベントハンドラの中で state を変更するのが最もよく見られるパターンです。
import { useState } from 'react';
function Toggle() {
const [isOn, setIsOn] = useState(false);
function handleToggle() {
setIsOn(prev => !prev);
}
return (
<div>
<p>現在の状態: {isOn ? 'ON' : 'OFF'}</p>
<button onClick={handleToggle}>トグル</button>
</div>
);
}
export default Toggle;イベントが起きると → ハンドラが実行され → state が更新され → 画面が再描画されます。この流れが React アプリの最も基本的なパターンです。
自分でやってみる #
簡単な入力フォームを作ってみます。ユーザが名前とメッセージを入力して「追加」を押すと、画面の下にメッセージが追加されるコンポーネントです。7 章 / 8 章でこれを広げていきます。
src/MessageForm.jsx を作ります。
import { useState } from 'react';
function MessageForm() {
const [name, setName] = useState('');
const [message, setMessage] = useState('');
const [lastSubmitted, setLastSubmitted] = useState(null);
function handleSubmit(e) {
e.preventDefault();
if (!name || !message) return;
setLastSubmitted({ name, message });
setName('');
setMessage('');
}
return (
<div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="名前"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div style={{ marginTop: '8px' }}>
<input
type="text"
placeholder="メッセージ"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<button type="submit" style={{ marginTop: '8px' }}>追加</button>
</form>
{lastSubmitted && (
<p style={{ marginTop: '12px' }}>
最後の入力: <strong>{lastSubmitted.name}</strong>. {lastSubmitted.message}
</p>
)}
</div>
);
}
export default MessageForm;src/App.jsx につなぎます。
import MessageForm from './MessageForm';
function App() {
return (
<>
<h1>メッセージフォーム</h1>
<MessageForm />
</>
);
}
export default App;名前とメッセージを入力して Enter または「追加」ボタンを押してみてください。最後に送信された値が下に表示され、入力フィールドは空になります。e.preventDefault() を外すと、フォームがリロードされて入力値が消えるのも実験してみてください。
lastSubmitted && (...) の部分が初めて見えるかもしれませんが、これは条件付きレンダリングです。値があれば見せて、なければ見せないパターンです。次の 7 章で詳しく扱います。練習問題 #
- 上の
MessageFormに「リセット」ボタンを追加し、1 回押すとlastSubmittedがnullに戻るようにしてみてください。<button type="button" onClick={() => setLastSubmitted(null)}>リセット</button>の形。type="button"を明示しないとフォーム送信として動作するので注意します。 - キーボードイベントの練習。
src/SearchBox.jsxを作り、<input>にonKeyDownハンドラを付けて、ユーザが Enter を押すと現在の入力値をalertで表示するように書いてみてください。if (e.key === 'Enter') alert(...)パターン。 - ハンドラを props として渡す練習。
Buttonコンポーネントを作ってlabelとonClickの 2 つの prop を受け取るようにし、親で同じButtonを 3 回使いながらそれぞれ別のハンドラを渡してみてください。クリックするとコンソールにそれぞれ別のメッセージが出るようにします。
一行まとめ:
onClickのような camelCase 属性でハンドラを登録する。関数を呼び出さずに渡す({handleClick}であって{handleClick()}ではない)。引数を渡すにはアロー関数で包む。ハンドラは最初の引数として合成イベントオブジェクトeを受け取る。e.preventDefault()でブラウザの既定動作を防げる。ハンドラも props として子に渡せる(onで始まる名前)。
次の章 #
また、MessageForm の例でちらっと見た {lastSubmitted && ...} パターンがそのまま次のトピックにつながります。次の第 7 章 条件付きレンダリングでは、画面の一部を state に応じて見せたり隠したり、別の姿に変えたりするさまざまなパターンを整理します。