ReactでonClickが一度だけ無反応になるバグとその対処法

Reactの開発において、「onClickが一度だけ無反応になる」という一見不可解な現象に遭遇したことはないでしょうか?特に、モーダルやドロップダウン、トグルなどのUIコンポーネントにおいてこのバグは潜んでおり、状態管理やイベント伝播に関する深い理解が求められます。

本記事では、このバグがどのような原因で発生するのか、具体例とともに解説し、その対処法を段階的に紹介していきます。開発中に同様の挙動に悩まされた方は、ぜひご参考ください。

1. よくある症状と再現手順

まず、どのような状況でこのバグが発生するのかを確認しましょう。典型的な症状は以下のとおりです。

  • ボタンやアイコンをクリックしても、最初の1回目だけ反応しない
  • 2回目以降は正常に反応する
  • 特定のコンポーネントを切り替えた後に、1回だけ onClick が動かない
  • イベントのバブリングを制御している場合に発生しやすい

以下のようなコードで、この挙動を再現することが可能です。

import React, { useState } from 'react';

const ToggleMenu = () => {
  const [open, setOpen] = useState(false);

  const handleToggle = (e) => {
    // event.stopPropagation(); ← これが原因になることもある
    setOpen((prev) => !prev);
  };

  return (
    <div onClick={() => setOpen(false)}>
      <button onClick={handleToggle}>Toggle Menu</button>
      {open && <div className="menu">メニュー内容</div>}
    </div>
  );
};

export default ToggleMenu;

event.stopPropagation()のコンポーネントでは、ボタンをクリックしても一度目だけメニューが開かないことがあります。

発生環境例

  • React 17以降
  • Chrome または Safari(モバイル含む)
  • イベントのラップがあるカスタムコンポーネント(例:Material UI)

2. 原因1:event.stopPropagationの副作用

event.stopPropagation() を使用すると、親要素へのイベント伝播を停止できます。しかし、このメソッドは慎重に使わなければ、逆に意図しない副作用を引き起こします。

以下は誤った使い方の例です。

const handleClick = (e) => {
  e.stopPropagation(); // ← これが onClick 無反応の原因
  setOpen(true);
};

stopPropagation() は、document レベルのクリック監視などと組み合わせた場合に、クリックを「無視」してしまうことがあります。その結果、onClick が反応しないように見えるのです。

対処法

  • 基本的に stopPropagation() の使用を最小限にする
  • useRefevent.currentTarget によるクリック範囲制御に置き換える

3. 原因2:初期状態と非同期の罠

Reactの状態管理では、useState による初期値の設定ミスも onClick の無反応を引き起こす要因となります。

たとえば、以下のように open の初期値が false であるにもかかわらず、レンダリングタイミングの都合で onClick がマウント前に呼び出されてしまうケースです。

const [open, setOpen] = useState(false);

useEffect(() => {
  // DOMが描画される前にクリックを検出してしまうケース
}, []);

また、非同期関数を使っている場合にも、1回目だけ setState が適用されないというバグのような挙動に遭遇することがあります。

対処法

  • 状態更新はできるだけ同期的に行う
  • イベント直後の状態に依存せず、useEffect で副作用を明示する

4. 原因3:useEffectと依存配列の設計ミス

useEffect の依存配列に状態を適切に含めない場合、期待した再レンダリングが起こらず、結果として onClick が機能しないように見えることがあります。

以下は失敗例です。

useEffect(() => {
  document.addEventListener('click', handleOutsideClick);
}, []); // ← openが依存配列にない

const handleOutsideClick = () => {
  if (open) {
    setOpen(false);
  }
};

このような場合、open の最新の値を参照できず、1回目のクリックイベントがすり抜けてしまいます。

対処法

  • 依存配列には状態値を正しく含める
  • useCallbackuseRef を組み合わせて関数の安定性を保つ

5. 原因4:イベントデリゲーションの干渉

親要素で onClick をハンドリングしていると、子要素の onClick が意図通りに動作しないことがあります。

以下のように、親が先に setOpen(false) を呼び出してしまい、子のクリックが即座に「消される」状態です。

<div onClick={() => setOpen(false)}>
  <button onClick={() => setOpen(true)}>Toggle</button>
</div>

このケースでは、ボタンをクリックしても、親が先に状態を変えてしまうため、open の状態が即時に false に戻ってしまいます。

解決策の一例

e.stopPropagation() を使うのではなく、外側のクリック検出を useEffect で制御する方がより安全です。

useEffect(() => {
  const handleClick = (e) => {
    if (!ref.current.contains(e.target)) {
      setOpen(false);
    }
  };
  document.addEventListener('click', handleClick);
  return () => document.removeEventListener('click', handleClick);
}, []);

6. デバッグのヒントと再発防止策

このような1回目だけ無反応になるバグは、状態とイベントのタイミングがズレることで生じます。複雑な UI コンポーネントでは、再現が難しいこともあります。

以下のようなデバッグ方法が有効です。

  • console.log でイベント発火の順序を確認
  • useRef を使ってイベントの対象要素を追跡
  • DevToolsでイベントリスナのバインド状況を確認
  • 再現最小コードを作成し、バグの切り分けを行う

再発防止のために心がけること

  • useEffect 依存配列の過不足をチェックする
  • onClick 処理の中で stopPropagation を使う場合は明示的にコメントを残す
  • 状態更新が遅延するような非同期処理は慎重に設計する

まとめ

「onClickが一度だけ無反応になる」というReactのバグのような挙動は、実際には状態管理やイベント伝播、ライフサイクルの理解不足によって引き起こされるものがほとんどです。

この記事で紹介したように、以下の原因を一つひとつ検証することで解決に近づけます。

  • stopPropagation() の副作用
  • 状態の初期値や非同期の扱い
  • useEffect の依存配列
  • 親要素によるクリックの取りこぼし
  • イベントの順序やスコープの設計ミス

Reactのイベントと状態は密接に関係しているため、1つのクリックが反応しないというだけでも多くの要因が関係しています。だからこそ、今回のような知識は日々の開発において非常に役立つはずです。

参考

タイトルとURLをコピーしました