Reactのイベント委譲:なぜあなたの「当たり前」はメモリを救っているのか
フロントエンドのアーキテクトとしてコードレビューをしていると、Reactのイベントハンドリングを「単なる属性の付与」としか捉えていないエンジニアにしばしば出会う。しかし、DOMの深淵を覗けば、Reactが採用しているイベント委譲(Event Delegation)という仕組みは、メモリ管理とパフォーマンス最適化における一つの芸術とも呼べる工夫であることに気づくはずだ。
今日は、Reactが舞台裏で何をやっているのか、そしてその挙動を理解することがなぜ「堅牢なアプリケーション」への近道なのかを深掘りしていこう。
—
1. 舞台裏の支配者:Reactのイベント委譲メカニズム
皆さんが普段書く `onClick` は、実はブラウザネイティブの `addEventListener` とは少し異なる。
React 17以前は、すべてのイベントを `document` レベルで捕捉していた。React 18以降は、イベントはルートコンポーネントがマウントされたノードにアタッチされるようになった。これが何を意味するか?
DOMノード一つ一つにイベントリスナーを貼るコストを、極限まで排除しているということだ。
もしリストアイテムが1,000個あるとして、それぞれに `addEventListener` を行えば、ブラウザのメモリ消費は跳ね上がる。Reactは、親要素でイベントを待ち受け、`event.target` を辿って「誰がイベントを発生させたか」を特定する。これにより、メモリ占有率を劇的に下げつつ、動的に追加される要素に対しても透過的にイベントをハンドリングできるのだ。
—
2. なぜこれがパフォーマンスの「壁」を突破するのか
上級エンジニアなら、ここで「レンダリング負荷」との関係に注目すべきだ。
Reactのイベントシステム(SyntheticEvent)は、イベントの発生を効率的にバッチ処理するための基盤でもある。もしネイティブなイベントをDOMに乱立させると、Reactの管理外でイベントが発火し、状態更新の競合やバッチ処理の阻害を引き起こすリスクがある。
特に、大規模な動的テーブルや無限スクロールを実装する際、個別のDOMにリスナーを貼るアプローチは「メモリリークの温床」になり得る。Reactのイベント委譲は、この問題をフレームワーク側で完全に抽象化してくれているのだ。
—
3. 現場で遭遇する「落とし穴」と回避策
理論は完璧でも、実戦では「非同期処理との競合」というバグが顔を出す。以下のコードを見てほしい。
import React, { useState } from ‘react’;
const EventTrapComponent = () => {
const [data, setData] = useState(null);
const handleClick = async (e) => {
// 重要な点: SyntheticEventは非同期処理に入ると再利用され、プールに戻される
// この時点で e.target は null になる可能性がある
const target = e.target;
// 非同期処理を待つ間にReactがレンダリングを更新すると、
// 参照が切れたり、意図しない要素を掴むリスクがある
const result = await fetchData();
// 安全な策: 必要な情報は非同期の前に抽出しておくこと
console.log(“ID:”, target.dataset.id);
setData(result);
};
return (
);
};
上級エンジニアへの教訓:
1. SyntheticEventの寿命を意識せよ: `e.target` 等の参照は、非同期処理(`await`)を挟むとReactによってクリアされる。必要な値は即座に定数へ退避させるのが鉄則だ。
2. `e.persist()` は過去の遺物: 古いReactの知見で `e.persist()` を使うケースが見られるが、現在では不要だ。単純に構造化されたデータを保持する設計にシフトしよう。
3. stopPropagationの挙動: Reactのイベント委譲は親要素で行われるため、Reactコンポーネント内での `e.stopPropagation()` は、Reactのイベントツリー内での伝播を止める。しかし、ネイティブな `addEventListener` でキャプチャフェーズにリスナーを登録している外部ライブラリがある場合、予期せぬ挙動になることがある。
—
4. アーキテクチャの視点:堅牢性を高めるために
大規模アプリケーションにおいて、イベントハンドリングの「設計」は、単なる実装の細部ではなく、状態管理の入り口である。
- 単一責務の原則: イベントハンドラ内にはロジックを書くな。ロジックはカスタムフックに逃がし、コンポーネントは「イベントを受けて関数を呼ぶだけ」の薄い層に保つ。
- バブリングを制御する設計: フォームの送信やモーダルの開閉など、イベントバブリングが複雑に絡むUIでは、あえてネイティブな `onPointerDown` などを使用するケースもゼロではない。だが、Reactのイベントシステムをバイパスする際は、そのコスト(メモリとパフォーマンスのトレードオフ)を常に自問自答すること。
まとめ
Reactのイベント委譲を「隠蔽された機能」として無視するのか、それとも「メモリを最適化するための武器」として使いこなすのか。この意識の差が、数年後のアプリのメンテナンス性やパフォーマンスに跳ね返ってくる。
「動けばいい」コードから「計算され尽くした」コードへ。Reactの内部挙動を愛する皆さんなら、今日のこの知見を、次回のコンポーネント設計に活かしてくれると信じている。
DOMを操作するのではなく、ブラウザの挙動を調律する。それがフロントエンド・アーキテクトの真の仕事だ。

コメント