テーマ、知りたいこと
悩んでいます。
A — コンポーネント内の個々の DOM ノードに直接付けるべきか?
B — コンポーネントのルートに集約するべきか?
C — ケースバイケースか?
背景、状況
独自のコンポーネント管理モジュール(MyReact)を実装しています。内部で状態変化を監視し、renderer.update() を呼んで要素を再描画する設計です。
ところが再描画後に 直接 addEventListener したイベントが消失する(=イベントハンドラが動作しなくなる)問題に直面しています。
ここで伺いたいのは、コンポーネント実装のアーキテクチャ観点です。
具体的には次の点について意見をいただければ助かります。
A — 再レンダリング(=DOM を新規に生成して置換する可能性がある)を前提にしたコンポーネントランタイムでは、イベントを個々の DOM ノードに直接付ける設計は許容されるか?
B — あるいは、React の Synthetic Event のように イベント処理を一箇所に集約(委譲/仮想イベント層)する設計が標準的か?
C — あるいは両者を条件で使い分ける(ケースバイケース)べきか?その判断基準は何か?
実装上の落とし穴(フォーカス管理、フォーム state、パフォーマンス、メモリリーク等)について具体的な注意点があれば教えてください。
以下に簡単な例コード(TypeScript)を添えます。現象と対策のイメージが伝わるようにしています。
例コード
例 A — コンポーネント内で直接 addEventListener を付与する(問題が出るケース)
問題点: update() が innerHTML 等で丸ごと置換する設計だと、以前バインドしたハンドラは新しいノードに引き継がれない。
ts
1// component.ts(簡略) 2export class Component { 3 private root: HTMLElement | null = null; 4 5 mount(container: HTMLElement, html: string) { 6 // 毎回 innerHTML で置換している想定 7 container.innerHTML = html; 8 this.root = container; 9 10 // 要素に直接イベントを結びつける 11 const btn = container.querySelector("button"); 12 if (btn) { 13 btn.addEventListener("click", () => { 14 console.log("clicked"); 15 }); 16 } 17 } 18 19 update(newHtml: string) { 20 // renderer.update が新しい DOM を生成して置換する場合、 21 // 前に addEventListener した要素は置き換えられイベントが消える 22 if (!this.root) return; 23 this.root.innerHTML = newHtml; 24 } 25 26 unmount() { 27 // ここで適切に removeEventListener を呼べる設計でないとメモリリークの恐れ 28 this.root = null; 29 } 30}
例 B — MyReact 側でイベントを集約して委譲する
利点: DOM を丸ごと差し替えてもイベントが失われない。イベントハンドラは MyReact のライフサイクルで一元管理できる。
ts
1// event-delegator.ts 2type Handler = (e: Event, target: Element) => void; 3 4export class EventDelegator { 5 private root: HTMLElement; 6 private handlers = new Map<string, Handler>(); // key: "click::selector" 7 8 constructor(root: HTMLElement) { 9 this.root = root; 10 // 代表的なイベントは root に一つだけ登録 11 this.root.addEventListener("click", (e) => this.handle(e)); 12 } 13 14 on(selector: string, handler: Handler) { 15 this.handlers.set(`click::${selector}`, handler); 16 } 17 18 private handle(e: Event) { 19 const target = e.target as Element | null; 20 if (!target) return; 21 // 単純な例: 全ハンドラを走査して closest で判定 22 for (const [key, h] of this.handlers) { 23 const [, sel] = key.split("::"); 24 const matched = target.closest(sel); 25 if (matched) h(e, matched); 26 } 27 } 28} 29 30// component.ts(Delegator 利用) 31const container = document.getElementById("app")!; 32const delegator = new EventDelegator(container); 33 34// コンポーネント描画とは独立してイベントを登録 35delegator.on(".btn-save", (e, target) => { 36 console.log("save clicked", target); 37}); 38 39// renderer.update() が DOM を差し替えても、イベントは delegator 側で処理される
例 C — 差分パッチ(morphdom 等?)を使って既存ノードをできるだけ再利用するアプローチ
DOM を完全置換せず、差分パッチで要素を更新することで 直接バインドしたイベントを保持できる場合がある。
ただし入力欄の selection や focus、カスタム要素の再初期化など注意点が多いため、差分方式だけで全ての問題が解決するわけではない。
最後に(問い)
あなたの現場では、どの設計を採用していますか?その理由と経験に基づく判断基準(規模、更新頻度、複雑さ、フォームの多さ、パフォーマンス要件など)を教えてください。
もし「委譲」を採る場合、どのレベルで委譲しますか(document / ルートコンテナ / コンポーネント単位)?またその判断基準は何ですか?
「直接付与を使い続ける」場合、update の影響をどう最小化していますか(差分パッチ、再バインド戦略、イベント記録と再適用など)?
実装例や過去に遭遇したトラブル、回避策があればぜひ共有してください。よろしくお願い致します。