🧠

イベントオブジェクトの target と currentTarget の違いとそれに伴う TypeScript の挙動を調べてみた

2021/01/14に公開

こんにちは。

突然ですが、 event.targetevent.currentTarget、どのように使い分けていますか?

そもそも違いあるの?と思うかもしれません。

単刀直入にいうと、大いにあります。使い方によってはバグを発生しかねません。ていうか僕の場合は発生しました。(二時間ぐらいかけてデバッグしたのはまた別の話)

今日は、この二つのプロパティーの違い、そしてどう TypeScript に影響するか、の話をします。

例えば、以下のようなコードがあるとします。

function App() {
  const onClick = (e) => {
    console.log(e.target);
    console.log(e.currentTarget);
  };
  return (
    <button onChange={onClick}>
      <strong>Click me</strong>
    </button>
  );
}

一見、同じ要素を出力すると思いがちですが、実は違います。

console.log(e.target); // <strong>Click me</strong>
console.log(e.currentTarget); // <button>...</button>

このように、e.target は strong 要素、e.currentTarget は button 要素になりました。

なぜかというと、こちらでも記載されている通り、

  • e.target はイベントを発生させる原因となった要素
  • e.currentTarget はイベントハンドラが実際に付与された要素

だからです。

これをちゃんと理解するには、イベントバブリングの仕組みを知る必要があります。

簡単にいうと、イベント発火後、直上の親要素に移動して同じ事をし、それが一番上の <html /> に辿り着くまで繰り返される、というメカニズムですね。

これを念頭に置いてもう一度 targetcurrentTarget の出力結果を見てみてください。

これぞアハ体験か、ってぐらい僕にはしっくりきました。

実際にクリックした要素(イベントを発生させる原因となった要素)が strong 要素で、

イベントバブリングして実際にイベントが発火された要素(イベントハンドラが実際に付与された要素)が button 要素となる。

ということですね。

そしてなぜ e.target を使うとバグが起こりやすいか、も先ほど説明した要因が関わってきます。

例えば上のコードで button 要素を指定したいのに、 strong 要素を指定しちゃう、とか。

要するに確実性がないということですね。

なので、できるだけ e.currentTarget を使いましょう、というのがこの記事で言いたいこと。

TypeScript

それに加え、 e.currentTarget を使うことで TypeScript の恩恵も大いに受けれます。

例えば、以下のようなコードがあるとします。

function App() {
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    // 通る
    console.log(e.currentTarget.value);
    // エラー
    console.log(e.target.value);
  }
  return (
      <button onClick={handleClick}>
        <strong>Click me</strong>
      </button>
  );

ちゃんとイベントの型を付けていても、e.target.value は、

Property 'value' does not exist on type 'EventTarget'

という風に怒られちゃいます。

(ここで const value = e.target as HTMLButtonElement という風にキャストして、、、みたいなのがよくあるパターン)

なぜかというと先ほども言った通り、

e.target はイベントを発生させる原因となった要素

なので、TypeScript にとっちゃどの要素が発生源か分かりようがないのです。

// 例:

function App() {
  return (
    <button>
      <span>
        <span>
          <strong>...</strong>
          {/* ここがイベント発生源だったとしても、TypeScript はわからないので自分でキャストする必要がある */}
        </span>
      </span>
    </button>
  );
}

ですが、 e.currentTarget

イベントハンドラが実際に付与された要素

なので TypeScript はちゃんと型付けされた情報が currentTarget なんだと理解できます。

面倒なキャスティングも不要、ということですね。

おまけ

target と currentTarget でいろいろ遊んでたら、ちょっと不思議な挙動に遭遇しました。

以下がそのコード:

export default function App() {
  const [name, setName] = React.useState("");
  function handleSetName(e: React.ChangeEvent<HTMLInputElement>) {
    // 通る
    console.log(e.currentTarget.classList);
    // これもなぜか通る???
    console.log(e.target.classList);
  }
  return (
    <form onSubmit={(e) => console.log(e)}>
      <input value={name} onChange={handleSetName} />
      <button type="submit">Submit</button>
    </form>
  );
}

onChange のイベントは全てイベントバブリングするはずなのに、なぜか input の onChange だけ、許されているんですよね。

いろいろ調べ回った結果、ここにその理由が述べられていました。

onChange ハンドラだけ e.target の挙動は異なり、ちゃんとイベントハンドラが登録されている要素になるのを保証してくれているので、問題ない

とのことです。

実際 react.d.ts をみてみると、

// index.d.ts:1153

interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
  target: EventTarget & T;
}

バブリングするのに、MouseEvent とは違い T で指定した要素型を target に連結してますね。

へぇ〜

ちなみに、他のフレームワークではどう型定義されているのかは分かりません!

最後まで読んでいただきありがとうございました〜

Discussion