React で検索キーワードフィルター入力をいい感じにしてみた


はじめに

タイトルの通り、ReactでInputフィールドに全角入力された時に綺麗に動かなかったので、IME対応メモです。

ざっくり環境はこんな感じです。

ライブラリ バージョン
React ^16.10.2
Material-UI ^3.8.3
react-redux ^6.0.0

仕様は以下のUML図の通り、

  1. Inputフィールドに入力
  2. Inputフィールドに入力した内容でAPIリクエスト
  3. APIのレスポンスをstoreに保存
  4. storeからpropsに渡してレンダリング

困ったこと

その1 全角入力問題

Material-UIのInput API、onChangeを使って、毎度APIにfetchしにいくのですが、
全角入力の場合、「テスト」と入力すると、「t、て、てs、てす、てすt、テスト...」全てでfetchされるわけです。。

入力速度によっては、「テスト」と入力しているのですが、
「テスt」のような入力が確定以前の状態でfetchした情報がstoreに保存されてしまうという問題が発生しました。(なんでーーー。)
画面上では、「テスト」で検索かけているつもりが、受け取っているデータは「テスt」でリクエストしたデータになっていておかしい。。。

フロントのみでフィルターして対応できれば良かったが、仕様の関係でAPIリクエストする必要がある...どうしようと言ったわけです。。
(技術的な負債を考慮したUIとか、まぁ、いろいろな問題はここでは置いておいて、)

その2 フロントが重い

Inputフィールドに変更が行われるたびに、onChangeが走るので、何度も何度もAPIリクエストが走ります。
重複した多重リクエストにフロント側の入力の挙動もかくかくなっちゃって良くない...

こういった、「いい感じ」にするための地味なところって結構辛い時多いです。
そもそも不要なリクエストが制限できれば理想。。。

回避策

その1 全角入力中はAPIリクエストしない

onCompositionStart&onCompositionEndメソッドを活用して回避しました。

原因としては、入力未確定状態でリクエスト投げて(しかも何度も)しまうことだったわけです。
全角入力開始時にonCompositionStartメソッドは発火、全握入力確定時にonCompositionEndメソッドが発火してくれるみたいです。

Material-UIの公式ドキュメントに用意されているメソッドではなくて、Inputタグが持つイベントのReactバージョンとして用意されているようです。
参照:公式Reactリファレンス(composition events)

具体的な回避策

onChangeメソッドで全角入力中かどうかのstate値を見て、falseの場合のみfetchするように変更
半角入力には特に制限はしていないので、半角入力時の多重リクエストは回避できません。。。泣


onChange = ({ target: { value: keyword }) => {
  this.setState({ keyword }, () => {
    if(!this.state.isIME) {
      this.props.fetchthis.state.keyword
    }
  }) 
}
onCompositionStart = () => {
  this.setState({ isIME: true })
}
onCompositionEnd = () => {
  this.setState({ isIME: false }})
}

...
<Input
  name="keyword"
  type="text"
  onChange={this.onChange}
  onCompositionStart={this.onCompositionStart}
  onCompositionEnd={this.onCompositionEnd}
  value={this.state.keyword}
>
...

これでなんとか理想の動作は確認できました。

その2 入力中をいい感じに取得して多重リクエストを回避

setTimeout()を使って、APIリクエストを行うようにしました。
onChangeが走ったタイミングで、入力中というステータスを管理するstateを生成。

setTImeout(fetch, 800)と、0.8秒など適当な間隔で、fetchさせるようにします。
(0.8秒以内の再リクエストはまだ入力中だと判断させる)
多重リクエストを避けるために、onChange発火タミング時にタイマーをリセットさせることで、回避させました。

これで、0.8秒以内に新しくリクエストが発生しそうな場合も前回リクエストを停止させます。

具体的な回避策

これで、0.8秒以内のテキスト入力時はAPIリクエストされずに、無駄な処理も走りません。
フロントもちょっと軽くすることができた。

onChange = ({ target: { value: keyword } }) => {
    clearTimeout(this.timer)
    this.setState(
      {
        isChange: true,
        keyword,
      },
      () => {
        this.timer = setTimeout(() => {
          this.setFilterState()
        }, 800)
      },
    )
  }

...
<Input
  name="keyword"
  type="text"
  onChange={this.onChange}
  value={this.state.keyword}
>
...

さいごに

処理重いって結構ユーザーにとって、嫌われる割合高いので(自分も嫌)、開発者として軽い動作だったり、滑らかなインタラクションというは気がけて行きたいなと思います。
setTImeout()とかって結構クールじゃない感じのメソッドという印象が強くて、あまり使いたくないんですが、今回は結構いい使い方できたのでは?と勝手に思ってます。

他にもっといいやり方あるとか、そもそもダメだろってところもあるかもなので、そいうのコメントとかしてもらえたら感謝です。。。

蛇足

Reactやっていると、componentDidUpdataとか、ループ並みに処理走っちゃうことがよくあるのですが、致し方ないものなのだろうか。。。
この辺綺麗にかけるように設計練り練りすべきなのだろうか...。

React初めてもうすぐ1年くらい経ちますが、まだまだ慣れないし、難しいなぁと思ってます。
(Hooksもやらなきゃ、redux卒業しないと...)