はじめに
ReactでUIを実装していると、次のような不具合に遭遇することがあります。
- メニューを開くボタンは表示されている
- クリックしてもメニューが開かない
- 常に発生するわけではなく、特定の条件でのみ発生する
このような症状を見ると、
- クリックイベントが発火していない?
- 透明な要素が重なっている?
-
z-indexの問題? - メニューが画面外に描画されている?
など、クリック周辺の原因を考えたくなります。
もちろん、原因の候補として仮説を立てることは重要です。
しかし、「クリックしても開かない」という症状でも、原因がクリック処理そのものにあるとは限りません。
このような不具合を切り分けるときに重要なのは、
症状から原因を推測し続けるのではなく、実際の処理経路を一つずつ確認し、「どこまで正常で、どこから期待とズレたのか」を探すこと
です。
この記事では、説明用のサンプルを使いながら、ReactのUI不具合を処理経路から切り分ける方法を整理します。
※掲載しているコード、データ構造、名称は説明用に単純化したサンプルです。実際のシステム固有の実装や仕様を示すものではありません。
まず「クリックできているか」を確認する
「ボタンを押しても何も起きない」場合、まずクリック処理まで到達しているかを確認します。
例えば、次のような処理を考えます。
const handleClickMenu = (item: Item) => {
setTargetItem(item);
};
イベントハンドラにブレークポイントを設定するなどして、クリック時に関数へ到達しているか確認します。
ボタンをクリック
↓
onClick
↓
handleClickMenu()
ここまで到達していることが確認できれば、少なくともイベント発火までは正常です。
つまり、
-
onClickが呼ばれていない - クリックイベントが要素まで届いていない
といった問題は、直接の原因ではない可能性が高くなります。
ここでクリック周辺だけを調べ続けるのではなく、次の処理へ進みます。
次に「クリック後に何が起きるか」を追う
クリックイベントまで正常であれば、その後の処理を確認します。
例えば、
setTargetItem(item);
によって、対象となるデータをstateへ保存する実装を考えます。
処理経路は、
クリック
↓
state更新
↓
?
↓
メニュー表示
となります。
次に確認したいのは、この ? の部分です。
例えば、stateに保存されたデータをもとに、表示可能なメニュー項目を生成しているとします。
const menuItems = [];
if (actions.rename.available) {
menuItems.push({ label: "名前を変更" });
}
if (actions.duplicate.available) {
menuItems.push({ label: "複製" });
}
if (actions.archive.available) {
menuItems.push({ label: "アーカイブ" });
}
ある条件で、この menuItems が空になるケースを考えてみます。
すると処理としては、
クリック成功
↓
state更新成功
↓
表示するメニュー項目が0件
となります。
この場合、「クリックしても何も起きない」のではありません。
クリック後の処理自体は進んでいることが分かります。
「何をもってメニューを開くのか」を確認する
さらに処理を追います。
例えば、メニューの開閉が次の条件で決まっているとします。
const isMenuOpen =
targetItem !== null &&
menuItems.length > 0;
クリックによって targetItem が設定されていても、
menuItems.length === 0
であれば、
true && false; // false
となり、メニューは開きません。
つまり、
「クリックに反応していない」のではなく、「クリック後の処理は正常に進んでいるが、最終的な表示条件を満たしていない」
という可能性が見えてきます。
ここまで確認できれば、調査対象をクリックイベントやCSSから、メニュー項目の生成条件や表示条件へ移せます。
では、なぜボタンは表示されているのか
ここで、さらに上流へ戻って考えてみます。
メニュー項目が0件になるのであれば、
なぜメニューを開くボタン自体は表示されているのか?
という疑問が生まれます。
例えば、ボタンの表示条件が次のようになっているとします。
const shouldShowMenuButton = Object.values(actions).some(
(action) => action.available,
);
一方、actions にメニューで使用する操作だけでなく、別のUIから実行する操作も含まれているとします。
const actions = {
rename: { available: false },
duplicate: { available: false },
archive: { available: false },
bookmark: { available: true },
};
ここでは、bookmark はメニューではなく、独立したUIから実行する操作だとします。
しかし、
Object.values(actions).some(
(action) => action.available,
);
では、bookmark も判定対象になります。
その結果、
メニュー外の操作だけが利用可能
↓
「利用可能な操作がある」と判定
↓
メニューボタンを表示
↓
クリック
↓
state更新
↓
メニュー項目を生成
↓
メニュー項目は0件
↓
メニューを開かない
という状態が発生します。
このように処理経路を整理すると、
ボタンは表示されるのに、クリックしてもメニューが開かない
という現象を説明できます。
「原因候補を考える」と「原因を確認する」は違う
最初に、
CSSかもしれない
イベントの問題かもしれない
と仮説を持つこと自体は問題ありません。
ただし、画面上の症状だけから原因を決めることはできません。
例えば、
クリック処理に到達したか
↓
stateは更新されたか
↓
表示するデータは生成されたか
↓
最終的な表示条件を満たしたか
と実際の処理経路を確認すれば、一つずつ可能性を切り分けられます。
「何が怪しそうか」を考えることと、「実際にどこまで正常に動いているか」を確認することは別です。
仮説を立てた後に、コードや実行結果によって検証することが重要です。
症状ではなく「最初に期待とズレた場所」まで戻る
画面上の症状が、
ボタンをクリックしてもメニューが開かない
だったとします。
このとき、メニューの開閉条件だけを変更して、
const isMenuOpen = targetItem !== null;
としてしまえば、中身が空のメニューが開く可能性があります。
それでは、目に見える症状を別の形に変えただけです。
処理を前から並べると、
ボタン表示判定
↓
クリック
↓
state更新
↓
メニュー項目生成
↓
開閉判定
となります。
このケースで最初に期待とズレているのは、最後の開閉判定ではありません。
メニュー項目が存在しない条件でも、メニューボタンを表示できてしまうことです。
つまり、問題が画面に現れた場所よりも、原因となる判断が上流に存在することがあります。
不具合を修正するときは、
どの条件を変更すれば症状が消えるか
だけでなく、
どこで最初に期待と実際がズレたのか
まで戻って確認することが重要です。
UIの不具合を処理経路から切り分ける
同様のUI不具合では、次のような順序で確認すると原因を切り分けやすくなります。
1. イベントは発火しているか
-
onClickなどのイベントハンドラまで到達しているか
2. stateは期待どおり更新されているか
- 操作後に必要な値がセットされているか
3. 表示に必要なデータは生成されているか
- 配列が空になっていないか
- 条件によって必要なデータが除外されていないか
4. 最終的な表示条件を満たしているか
-
isOpenなどの値が、どの条件から決まっているか
5. さらに上流で、なぜその状態になったのか
- 最終的な条件だけを変更するのではなく、どこで最初に期待と実際がズレたのか
「クリックしても開かない」という症状でも、原因がクリック処理にあるとは限りません。
イベント
↓
state
↓
データ生成
↓
表示条件
と処理経路を順番に確認し、
「どこまで正常に動いていて、どこから期待とズレたのか」
という境界を探すことで、調査範囲を絞りやすくなります。
まとめ
「クリックしても開かない」という症状だけを見ると、クリックイベントやCSSを疑いたくなります。
しかし、症状が現れている場所と、最初に期待とズレた場所が同じとは限りません。
UIの不具合を調査するときは、
イベント
↓
state
↓
データ生成
↓
表示条件
のように処理経路を順番に確認し、
「どこまで正常に動いていて、どこから期待とズレたのか」
という境界を探すことで、原因を切り分けやすくなります。
さらに、最終的な条件だけを変更するのではなく、
最初に期待と実際がズレた判断まで戻る
ことを意識すれば、目に見える症状だけを隠す修正も避けやすくなります。
症状から原因を決めつけるのではなく、
どの入力が、どの処理を通り、どこで初めて期待値と実際の値がズレたのか
を確認することが、不具合調査では重要だと考えています。