⚙️ JavaScript

JavaScriptのaddEventListenerが動かない原因|nullエラーを順番に解決

  • ボタンをクリックしても反応せず、Consoleに「Cannot read properties of null (reading ‘addEventListener’)」と表示されることがあります。このエラーは、イベントを登録する対象の要素が見つからないまま、addEventListener()を呼び出したときによく発生します。本記事では、原因を推測で直すのではなく、Console、セレクター、読み込み順、動的に追加される要素の順番で切り分ける方法を解説します。

    addEventListenerのnullエラーは何を意味する?

    次のコードでは、HTMLからid="menu-button"の要素を探し、クリックイベントを登録しています。
    const button = document.querySelector("#menu-button");
    
    button.addEventListener("click", () => {
      console.log("クリックされました");
    });
    document.querySelector()は、一致する要素があれば最初の要素を返します。しかし、一致する要素がなければnullを返します。その状態でnull.addEventListener()を実行しようとするため、TypeErrorになります。 つまり、最初に調べるべきなのはaddEventListener()の書き方ではなく、直前の要素取得が成功しているかどうかです。

    最初にConsoleでエラーの場所を確認する

    Chromeでは、ページ上で右クリックして「検証」を開き、Consoleタブを確認します。エラーの右側に表示されるファイル名と行番号をクリックすると、問題が起きたコードへ移動できます。 原因を確認するため、取得した値を一度出力します。
    const button = document.querySelector("#menu-button");
    
    console.log(button);
    • 対象のHTML要素が表示される:要素取得には成功しています。
    • nullと表示される:セレクターか読み込み順に問題があります。
    • この行より前に別のエラーがある:先に発生したエラーで処理が止まっています。
    Consoleに複数のエラーがある場合は、基本的に一番上のエラーから確認します。前の処理が止まった影響で、後続のエラーが発生していることがあるためです。

    原因1:CSSセレクターがHTMLと一致していない

    querySelector()にはCSSと同じ形式のセレクターを渡します。id#class.を付けます。
    <button id="menu-button" class="menu-button">
      メニューを開く
    </button>
    // idで探す
    const buttonById = document.querySelector("#menu-button");
    
    // classで探す
    const buttonByClass = document.querySelector(".menu-button");
    次のような小さな違いでも、要素は取得できません。
    • #menu-button#menu_buttonの違い
    • menu-button#.を付け忘れている
    • HTMLではmenuButton、JavaScriptではmenubuttonになっている
    • 別ページには対象のボタン自体が存在しない
    DevToolsのElementsタブで対象要素を選び、実際のidclassを確認すると早く切り分けられます。

    原因2:HTMLが作られる前にJavaScriptを実行している

    セレクターが正しくても、<head>内のスクリプトが先に実行されると、下に書かれたボタンはまだDOMに存在しません。
    <head>
      <script src="main.js"></script>
    </head>
    <body>
      <button id="menu-button">メニューを開く</button>
    </body>
    外部JavaScriptにはdeferを付けると、HTMLの解析後にスクリプトを実行できます。
    <head>
      <script src="main.js" defer></script>
    </head>
    deferを付けられない構成では、DOMContentLoadedを利用できます。このイベントは、HTML文書の読み込みと解析が完了した時点で発生します。
    document.addEventListener("DOMContentLoaded", () => {
      const button = document.querySelector("#menu-button");
    
      button.addEventListener("click", () => {
        console.log("クリックされました");
      });
    });
    スクリプトタグを</body>の直前に置く方法もあります。ただし、複数のファイルがあるサイトでは、読み込み意図が分かりやすいdeferを使うと管理しやすくなります。 ローカル環境でも、対象のボタンより前にJavaScriptを実行して同じ状態を再現しました。ボタンがまだ作られていない時点では、querySelector()の結果がnullになり、addEventListener()を呼び出した箇所でエラーが発生します。
    修正前:ボタン取得前にaddEventListenerを実行してnullエラーが表示されたConsole画面
    修正前:対象要素が作られる前にJavaScriptを実行し、Consoleにnullエラーが表示された状態
    ボタンを先に記述してからJavaScriptを実行するように変更すると、要素を取得でき、クリックイベントが正常に動作しました。
    修正後:ボタンをクリックしてイベントが正常に実行された画面とConsoleの結果
    修正後:HTMLのボタンを先に読み込み、クリックイベントが正常に実行された状態

    原因3:対象要素がないページでも同じJavaScriptを読み込んでいる

    WordPressなどでは、同じJavaScriptファイルを全ページで読み込むことがあります。トップページにはメニューボタンがあっても、別のテンプレートには存在しない場合、要素取得はnullになります。 要素があるページでだけ処理したい場合は、存在確認を入れます。
    const button = document.querySelector("#menu-button");
    
    if (button) {
      button.addEventListener("click", () => {
        console.log("クリックされました");
      });
    }
    オプショナルチェーンを使うと短く書けます。
    document
      .querySelector("#menu-button")
      ?.addEventListener("click", () => {
        console.log("クリックされました");
      });
    ただし、必ず存在するはずの要素にこの書き方を使うと、HTMLの記述ミスを見逃しやすくなります。ページによって存在しないことが正常な要素に使い、必須要素の場合はエラーとして確認するなど、目的に応じて使い分けます。

    原因4:イベント名や関数の渡し方が間違っている

    addEventListener()へ渡すイベント名にはonを付けません。クリックなら"click"です。
    // 間違い
    button.addEventListener("onclick", handleClick);
    
    // 正しい
    button.addEventListener("click", handleClick);
    登録時には関数を実行するのではなく、関数そのものを渡します。
    function handleClick() {
      console.log("クリックされました");
    }
    
    // 間違い:この場で関数を実行している
    button.addEventListener("click", handleClick());
    
    // 正しい:関数を渡している
    button.addEventListener("click", handleClick);
    引数を渡したい場合は、アロー関数の中から呼び出します。
    button.addEventListener("click", () => {
      handleMenu("open");
    });

    原因5:あとから追加した要素へイベントを登録している

    APIの取得結果やボタン操作によってHTMLを追加する場合、追加前にquerySelector()を実行しても要素は見つかりません。
    const item = document.querySelector(".new-item");
    console.log(item); // 追加前なのでnull
    
    list.insertAdjacentHTML(
      "beforeend",
      '<button class="new-item">追加されたボタン</button>'
    );
    要素を追加したあとにイベントを登録するか、親要素でイベントを受け取る「イベント委譲」を使います。
    const list = document.querySelector(".item-list");
    
    list.addEventListener("click", (event) => {
      const button = event.target.closest(".new-item");
    
      if (!button || !list.contains(button)) {
        return;
      }
    
      console.log("追加されたボタンがクリックされました");
    });
    親の.item-listは最初から存在するため、あとから追加された.new-itemのクリックも処理できます。複数のボタンへ同じイベントを個別登録しなくてよい点もメリットです。

    イベントは動いているのに画面が変わらない場合

    フォーム送信でページが再読み込みされている

    form内のボタンは、種類を指定しないと送信ボタンとして動作することがあります。単なる画面操作用ならtype="button"を指定します。
    <button type="button" id="menu-button">
      メニューを開く
    </button>
    フォーム送信処理をJavaScriptで制御する場合は、submitイベントで既定動作を止めます。
    const form = document.querySelector("#contact-form");
    
    form.addEventListener("submit", (event) => {
      event.preventDefault();
      console.log("入力内容を確認します");
    });

    クリックされた場所が想定と違う

    アイコンやspanを含むボタンでは、event.targetが内側の要素になることがあります。リスナーを登録した要素を取得したい場合はevent.currentTargetを使います。
    button.addEventListener("click", (event) => {
      event.currentTarget.classList.toggle("is-open");
    });

    そのまま試せる完成例

    次の例は、ボタンを押すと案内文の表示状態を切り替えます。外部ファイルで使う場合は、JavaScript側をmain.jsへ保存し、スクリプトにdeferを付けます。

    HTML

    <button
      type="button"
      class="help-button"
      aria-expanded="false"
      aria-controls="help-message"
    >
      解決方法を表示
    </button>
    
    <p id="help-message" hidden>
      Consoleとセレクターから順番に確認しましょう。
    </p>
    
    <script src="main.js" defer></script>

    JavaScript

    const button = document.querySelector(".help-button");
    const message = document.querySelector("#help-message");
    
    if (button && message) {
      button.addEventListener("click", () => {
        const isOpen = button.getAttribute("aria-expanded") === "true";
    
        button.setAttribute("aria-expanded", String(!isOpen));
        message.hidden = isOpen;
        button.textContent = isOpen
          ? "解決方法を表示"
          : "解決方法を閉じる";
      });
    }
    この例では、要素の存在確認をしてからイベントを登録しています。また、aria-expandedの値と表示状態を揃えることで、ボタンの状態が支援技術にも伝わるようにしています。

    動かないときの確認チェックリスト

    1. DevToolsのConsoleで最初のエラーと行番号を確認する
    2. console.log()で取得した要素がnullではないか確認する
    3. HTMLのidclassとセレクターを見比べる
    4. 外部スクリプトにdeferを付けるか、DOM生成後に実行する
    5. 対象要素が存在しないページでは、存在確認を入れる
    6. イベント名にonを付けていないか確認する
    7. コールバックをhandleClick()ではなくhandleClickとして渡す
    8. 動的要素なら、追加後の登録またはイベント委譲を使う
    9. フォーム送信やリンク移動で結果が消えていないか確認する
    10. JavaScriptファイルのURLが404になっていないかNetworkタブで確認する

    まとめ

    addEventListener()が動かないときは、最初に対象要素を取得できているか確認します。querySelector()nullなら、セレクター、JavaScriptの読み込み順、ページ内に要素が存在するかを順番に調べます。要素を取得できている場合は、イベント名、関数の渡し方、フォームの既定動作、動的要素の追加タイミングを確認しましょう。確認順序を決めておくと、コードをむやみに書き換えずに原因を特定できます。

    参考資料

掲載内容は最終確認日時点の情報です。バージョンや環境によって表示・手順が異なる場合があります。重要な変更の前にはバックアップを取得してください。