reactjs

onFocusでsetStateするとonClickが実行されない

こんな感じの仕様のドロップダウンを実装するとします。

  • クリックだけでなく、 Tab でフォーカスしたときにも開く
  • クリックの場合はイベントをトラッキングしてからドロップダウンを開く

色々足りませんがこんな感じのコードを書いたとします:

class Dropdown extends React.Component {
  open() {
    this.setState({ isOpen: true });
  }

  render() {
    return (
      <div
        tabIndex="0"
        onClick={e => {
          trackClickEvent(e).then(() => this.open());
        }}
        onFocus={this.open.bind(this)}
      >
        {/* ... */}
      </div>
    )
  }
}

しかし、このコードは意図したように動きません。クリック時に onFocus だけが実行されて onClick が実行されないのです。何故でしょうか?

(おそらく)onClick が実行されない理由

まずブラウザの仕様として(要出典) フォーカス可能な要素をクリックすると、先に FocusEvent が発生し、その後でクリックによる MouseEvent が発生します。つまり onFocus が先に実行されます。

そして(React の中を追っていないので正確なところは分からないのですが) onFocus の中で setState すると次のフレームで再レンダリングされ、その過程で何かがあって onClick が実行されなくなります。

対処方法

onFocus の前に onMouseDown が実行されるので、 onClick の代わりにそちらを使うという方法があります:

      <div
        tabIndex="0"
-       onClick={e => {
+       onMouseDown={e => {
          trackClickEvent(e).then(() => this.open());
        }}
        onFocus={this.open.bind(this)}
      >

この場合たしかに onMouseDown が先に実行されるのですが、なぜか onFocus が実行されます :thinking:

おわりに

ちなみに同じく VDom を使っている hyperapp でも同じような現象が発生するので、 React の問題というよりはブラウザの挙動なのではないかと思います。