🍣

DOM操作でハマりがちな4つのポイントを整理する

に公開

フロントエンド開発をしていると、ReactやVueなどのフレームワークを使う場面が多くなりますが、結局その裏側で動いているのはDOM操作です。

特に業務でレガシーな画面やVanilla JSを触ると、「イベント周りの理解が曖昧だと一気に詰む」ことがよくあります。

今回は、実務でよく使う以下の4つを整理します。

  • フォームイベントと preventDefault
  • inputchange の違い
  • イベントのバブリング
  • イベントデリゲーション

フォームイベントと 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 の裏でも同じことが起きています。


inputchange の違い

これも地味にハマります。

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 → 確定後
  • バブリング → イベントは親に伝わる
  • デリゲーション → 親でまとめて処理

参考

https://analyzegear.co.jp/blog/2528
https://qiita.com/yokoto/items/27c56ebc4b818167ef9e
https://developer.mozilla.org/ja/docs/Learn_web_development/Core/Scripting/Event_bubbling
https://www.udemy.com/course/the-web-developer-bootcamp-2021-japan/?srsltid=AfmBOorfOBv_gNkmtAy0M-FMxnnaNEYAsRDVNFM5kc6E1KB62f1yiGsT

Discussion