🍣
DOM操作でハマりがちな4つのポイントを整理する
フロントエンド開発をしていると、ReactやVueなどのフレームワークを使う場面が多くなりますが、結局その裏側で動いているのはDOM操作です。
特に業務でレガシーな画面やVanilla JSを触ると、「イベント周りの理解が曖昧だと一気に詰む」ことがよくあります。
今回は、実務でよく使う以下の4つを整理します。
- フォームイベントと
preventDefault -
inputとchangeの違い - イベントのバブリング
- イベントデリゲーション
フォームイベントと preventDefault
まず一番最初にぶつかるのがこれ。
フォームの送信イベントはデフォルトで「ページ遷移」が発生します。
<form id="form">
<input type="text" name="username">
<button>送信</button>
</form>
const form = document.querySelector("#form");
form.addEventListener("submit", (e) => {
console.log("送信された");
});
この状態だと、console.log が一瞬出たあとページがリロードされます。
なぜ起きるか?
ブラウザには「デフォルト動作」があるからです。
- form → 送信してページ遷移
- aタグ → リンク遷移
- checkbox → 状態切り替え
これを止めるのが preventDefault() です。
form.addEventListener("submit", (e) => {
e.preventDefault();
console.log("ページ遷移しない送信");
});
実務での意味
これがないと:
- SPAが崩壊する
- API送信前に画面がリロードされる
- 状態管理が全部消える
Reactでいう onSubmit の裏でも同じことが起きています。
input と change の違い
これも地味にハマります。
input イベント
「入力が変わった瞬間」に発火
const input = document.querySelector("input");
input.addEventListener("input", () => {
console.log("リアルタイム更新");
});
特徴:
- 1文字打つごとに発火
- 即時反映に向いている
👉 検索ボックス・バリデーション・ライブプレビューなど
change イベント
「値が確定したとき」に発火
input.addEventListener("change", () => {
console.log("確定した");
});
特徴:
- フォーカスが外れたタイミング
- Enter押下など
👉 フォーム送信前のチェックなど
実務での使い分け
| ケース | イベント |
|---|---|
| リアルタイム検索 | input |
| 入力確定後の処理 | change |
Reactの onChange は実は input 相当の挙動なので注意です。
イベントのバブリング
DOMイベントは「発生した要素だけで終わらない」のが重要ポイントです。
例
<div id="parent">
<button id="child">クリック</button>
</div>
document.querySelector("#child").addEventListener("click", () => {
console.log("child");
});
document.querySelector("#parent").addEventListener("click", () => {
console.log("parent");
});
ボタンをクリックするとコンソールに以下が表示されます。:
child
parent
と出ます。
なぜ?
イベントは「内側 → 外側」に伝播するからです。
これを バブリング(bubbling) といいます。
制御する方法
e.stopPropagation();
document.querySelector("#child").addEventListener("click", (e) => {
e.stopPropagation();
console.log("childだけ");
});
これで親にイベントが伝わらなくなります。
実務での使いどころ
- モーダルの外クリックで閉じる
- 親要素でまとめてイベント管理
- 意図しないイベント伝播の防止
イベントデリゲーション
バブリングを理解すると、次に来るのがこれ。
「イベントは親にも伝わる」=「親でまとめて処理できる」
NGパターン
<div id="parent">
<button id="child">クリック</button>
</div>
const buttons = document.querySelectorAll("button");
buttons.forEach(btn => {
btn.addEventListener("click", () => {
console.log("クリック");
});
});
問題点:
- 要素が増えるとリスナーも増える
- 動的追加に対応できない ※最初からある要素に限定されるので、jsで要素を追加してもaddEventLinsterができない
デリゲーション
デリゲーション:(意)権限委譲
document.querySelector("#parent").addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
console.log("ボタンがクリックされた");
}
});
メリット
- リスナー1つでOK
- 動的追加にも対応
- パフォーマンス良い
よく使う書き方
if (e.target.matches(".btn")) {
// 特定クラスのみ処理
}
まとめ
DOMイベント周りは「なんとなく動く」状態で使っていると、規模が大きくなった瞬間に破綻します。
今回のポイントを整理すると:
-
preventDefault→ デフォルト動作を止める -
input→ リアルタイム -
change→ 確定後 - バブリング → イベントは親に伝わる
- デリゲーション → 親でまとめて処理
参考
Discussion