3秒でわかる
React が各ブラウザの DOM イベントを包んで揃えたイベントオブジェクト。onClick などの引数として渡り、ブラウザ差を意識せず処理を書けます。
もう少し詳しく
どういうものか
React のコンポーネントで onClick={(e) => ...} と書いたときに受け取る e は、ブラウザが作ったネイティブのイベントそのものではなく、React が包み直した SyntheticEvent です。type target preventDefault() stopPropagation() といった見慣れた API をそのまま備えつつ、ブラウザごとの細かな差異を吸収しています。
元のネイティブイベントが必要なときは e.nativeEvent から取り出せます。
なぜ必要か
同じクリックでも、古いブラウザとの間でプロパティ名や挙動が食い違うことがありました。React はイベントを一枚かぶせることで、どの環境でも同じオブジェクトが届くことを保証しています。
もう1つは性能上の理由です。React はすべての onClick を個々の DOM 要素に登録するのではなく、ルートに1つだけリスナを置き、そこから該当コンポーネントへ配送します。要素が1000個あってもリスナは1つで済み、この配送の途中で作られるのが SyntheticEvent です。
具体例
function SearchForm() {
const [q, setQ] = useState("");
const handleSubmit = (e) => {
e.preventDefault(); // ページ遷移を止める
console.log(e.type); // submit
console.log(e.nativeEvent); // 元の DOM イベント
search(q);
};
return (
<form onSubmit={handleSubmit}>
<input value={q} onChange={(e) => setQ(e.target.value)} />
<button type="submit">検索</button>
</form>
);
}つまずきやすいところ
フォームの送信で e.preventDefault() を書き忘れると、画面が丸ごと再読み込みされて状態が消えます。React が止めてくれるわけではありません。
イベントの登録先も誤解されがちです。React 17 以降、リスナはドキュメントではなくアプリのルート要素に付きます。document.addEventListener で自前に足したリスナと React のハンドラを混ぜると、発火順が期待とずれることがあります。SyntheticEvent の stopPropagation() は React の配送を止めるだけで、ルートより外側に付いたネイティブのリスナには効きません。
React 16 まではイベントオブジェクトが使い回され (イベントプーリング)、setTimeout の中で e.target を見ると空になる問題がありました。React 17 で廃止されているため、現在は非同期処理の中でも参照できます。古い記事の e.persist() は不要です。
覚え方
「React は生のイベントを一度受け取り、包み直して配る」。包み紙の中身を見たいときだけ nativeEvent を開けます。