👋
なぜかクリックイベントが暴発する?React+MUIのTableRowでハマった話
なぜかクリックイベントが暴発する?React+MUIのTableRowでハマった話
削除ボタン押したのに、次に別の場所クリックするとモーダルが開く……。その違和感、ちゃんと対処できます。
「TableRowが勝手に反応する」現象、起きてませんか?
React × MUI を使ってテーブルを作っていて、こんな経験ありませんか?
- TableRowをクリックするとモーダルが開くようにしている
- でも、行内にあるボタン(削除など)を押したあと、他の場所をクリックするとモーダルが勝手に開く
<TableRow onClick={() => handleRowClick(row)}>
<TableCell>{row.name}</TableCell>
<TableCell>
<DeleteButton onClick={(e) => { e.stopPropagation(); handleDelete(row); }} />
</TableCell>
</TableRow>
「event.stopPropagation()
してるのに、なぜ?」と思った方。僕もそうでした。この記事では、 なぜその現象が起きるのか? と、 どう防げばいいのか? をまとめます。
なぜ stopPropagation() だけでは不十分なのか
ReactはDOMのイベントを"合成イベント(SyntheticEvent)"という仕組みで扱っています。
この合成イベント、以下のような特徴があります:
- イベントは一度プールされ、必要に応じて遅延的に処理されることがある
- MUIなどのコンポーネントライブラリも内部で色々やってる
結果として、「その時止めたつもりでも、React的には“あとで効いてくる”」ということが発生します。
これが “削除ボタン押したのに、その後でモーダルが開く” 原因です。
解決法:TableRowのonClickにフィルタを入れる
target.closest('td')
を使って「TableCell内クリックのみ許可」
✅ <TableRow
onClick={(event) => {
const target = event.target as HTMLElement;
// TableCell(td)内のクリックだけ許可
if (!target.closest('td')) return;
handleRowClick(row);
}}
>
closest('td')
は、クリックされた要素から親をたどって最初の<td>
を探します。
- つまり「TableCell内をクリックしたとき」だけ、モーダルを開く
- ボタン・アイコンなどで
<td>
の外にある場合はスルーできる
✅ TableRow全体ではなく「空白部分のみ」反応させたい場合
<TableRow
onClick={(event) => {
if (event.target !== event.currentTarget) return;
handleRowClick(row);
}}
>
この方法では、TableRowの“純粋な背景部分”しか反応しません。好みに合わせて使い分けましょう。
なぜこの対策が効くのか?
closest()
はセレクタベースの親探索
クリックされた要素(たとえば<svg>
)が<td>
もしくは<td>
内にあれば、closest('td')
でちゃんとマッチしてくれます。
event.target !== event.currentTarget
これは、クリックされた要素がRowそのものだったか?を判定するシンプルな方法です。
最後に:この記事で伝えたいこと
- MUI + Reactは便利だけど、ちょっとしたUXでハマりポイントがある
-
stopPropagation()
では不十分なケースがある(SyntheticEventの罠) -
closest('td')
やevent.currentTarget
の知識があると、スマートに防げる
「TableRowのonClick、なぜか暴発する」問題で困っていた方が、この記事でスッキリできたら嬉しいです。
Discussion
この場合は クリックされた要素が
<td>
もしくは<td>
内にあれば、 ではないでしょうか……?そうですね💦
ご指摘ありがとうございます!
修正いたしました!