質問するログイン新規登録

意見交換

5回答

325閲覧

「再レンダリングで DOM を差し替えるコンポーネント実装」におけるイベント設計 — DOM に直接付与すべきか、フレームワーク的に集約すべきか?

kuzuha

総合スコア3

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

TypeScript

TypeScriptは、マイクロソフトによって開発された フリーでオープンソースのプログラミング言語です。 TypeScriptは、JavaScriptの構文の拡張であるので、既存の JavaScriptのコードにわずかな修正を加えれば動作します。

0グッド

2クリップ

投稿2025/11/20 03:52

編集2025/11/20 03:55

0

2

テーマ、知りたいこと

悩んでいます。

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 の影響をどう最小化していますか(差分パッチ、再バインド戦略、イベント記録と再適用など)?

実装例や過去に遭遇したトラブル、回避策があればぜひ共有してください。よろしくお願い致します。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

回答5

#1

maisumakun

総合スコア146967

投稿2025/11/20 04:14

あなたの現場では、どの設計を採用していますか?

Reactを利用しているので、「その仕組みに乗っかる」のほぼ一択です。

(そもそも、「JavaScriptからDOMを構築するための、フレームワークレベルから作る」というのは、ごく試験的な規模、あるいは超大規模で社内フレームワークを作れるリソースがあるといった、限られた条件でしか選択されないような気はします)

#2

miyabi-sun

総合スコア21572

投稿2025/11/20 05:50

独自のコンポーネント管理モジュール(MyReact)を実装しています。

なるほど
ReactやVue.jsがあるのに、なんでオレオレJSフレームワークなんて実装してんの?みたいな野暮は言いません

A - Cのプランを読みましたが、どれも良いと思います。
どれをえいや!で決断するか?だけかの話でしょうね。

A — 再レンダリング(=DOM を新規に生成して置換する可能性がある)を前提にしたコンポーネントランタイムでは、イベントを個々の DOM ノードに直接付ける設計は許容されるか?

別に許容されるとは思いますが、
addEventListenerに直接無名関数を流し込むような設計は絶対にやめておくべきですね。

js

1// どっかの変数スコープに持たせる 2const updateDom = () => { 3 // なんか操作 4} 5my_element.addEventListener("click", updateDom)

このように関数の実体さえ変数スコープに残っていれば、
消えても再度付与し直せば良いだけの話になるかと思います。
この辺はライブラリの設計の腕の見せどころになるでしょう。

それを突き詰めるとBプランのどっかで関数・イベントリスナーを一元管理する方針と混ざり合う感じに落ち着きそうな気がしますね。

例 C — 差分パッチ(morphdom 等?)を使って既存ノードをできるだけ再利用するアプローチ

Reactが採用しているシャドーDOMは遅えよ!とは結構言われていますね。
既存ノードを出来るだけ再利用するJSフレームワークは探せばそれなりに出てくると思います。

あなたの現場では、どの設計を採用していますか?

Reactとかの既製品に丸投げだよ!
そんなところで無駄に責任持ちたくない!

#3

kuzuha

総合スコア3

投稿2025/11/20 06:27

#1
いつもありがとうございます。今回の質問の意図は「ごく試験的な規模」に近いと思います。「そのライブラリを世に公開したい」みたいな野心的な試みではなく、「ライブラリは使わずにサイトを作りたい」という感じです。

細かい経緯としては、先日の質問 (https://teratail.com/questions/q0s4p7k7o4qehk) のように、「npmパッケージを提供したい。その場合他のライブラリは入れない方がいいっぽい。(React とかも入れない方がいいっぽい。)」と考えてコードを書いていました。
結局ご回答を受けて「別に使ってもいいのかも」と考え直したものの、「まぁもうここまでライブラリなしで作ってきたし、勉強も兼ねてこのまま進めるか」という経緯を経て、「ライブラリは使わずにサイトを作りたい」という感じです。

#4

kuzuha

総合スコア3

投稿2025/11/20 06:47

編集2025/11/20 06:47

#2
ありがとうございます。ネット検索でたまにご回答を拝見しますが、どれも面白く、めっちゃ高レベルで大変勉強させて頂いております。

今回も詳細にありがとうございます。

なんでオレオレJSフレームワークなんて実装してんの?

emoji-picker-element のようにHTML要素を挿入できるパッケージ「iddb-element」を作っていて、この「iddb-element」側で npm install react と書いてしまうと、「iddb-element」を使う側の npm install react と衝突しそうだとAIに聞いたので、じゃぁ要素管理系の処理は自分で実装しないといけないのかぁ。と考えた次第です。

また README を見てみると "Framework and bundler not required" と書いてあるので、これを真似ようという意図もあります。

どれをえいや!で決断するか?だけかの話でしょうね。

なるほど、意外と大差ないのですね。イベントリスナーを一元管理する注意点もありがとうございます。すでに現状でまさに無名関数でとりあえず実装している箇所があるので、早速 TODO をつけておきました。

そんなところで無駄に責任持ちたくない!

なるほど。各社やチームがプロジェクトごとに独自に作っていることも少なくないのかな、なんて思っていました(業界未経験者です)が、React を使うだけでその処理の範囲は免責されそうですね。

#5

yambejp

総合スコア118219

投稿2025/11/20 07:52

typescriptでの運用はちょっとわからないですが、自前フレームワークで設定したイベントをDOMの書き換えに影響されないようにするにはdocumentにaddEventListenerすることです

例えばこんな感じ

javascript

1<script> 2document.addEventListener('click',e=>{ 3 const t=e.target; 4 if(t.matches('.load')){ 5 document.body.appendChild(tmp.content.cloneNode(true)); 6 } 7 if(t.matches('.reset')){ 8 [...document.querySelectorAll('[data-value]')].forEach(x=>x.remove()); 9 } 10 if(t.matches('[data-value]')){ 11 console.log(t.dataset.value); 12 } 13}); 14</script> 15<input type="button" value="load" class="load"> 16<input type="button" value="reset" class="reset"> 17<template id="tmp"> 18<input type="button" value="a" data-value="aaa"> 19<input type="button" value="b" data-value="bbb"> 20<input type="button" value="c" data-value="ccc"> 21</template>

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

この意見交換はまだ受付中です。

会員登録して回答してみよう

アカウントをお持ちの方は

関連した質問