Techtouch Developers Blog

テックタッチ株式会社の開発チームによるテックブログです

リアルタイム検索最適化:Reactアプリにdebounce処理を組み込む

はじめに

テックタッチのフロントエンドエンジニアの ozaan (@shzawa) です。関西 (兵庫県姫路市) 在住のため、普段はフルリモートのメンバーとしてサービス開発に取り組んでいます。

最近はスクラム開発の一環でバックエンドのコード (golang) を触らせてもらっていました。golang だと書き方が統一されているためか GitHub Copilot の補完機能がかなり効くので書いていて面白かったです。

そもそも debounce 処理って?

対象のイベントが発生してから指定した時間が経過するまでは、同じイベントの発生を抑制する仕組みです。

input 要素の onChange イベントのような短い間隔で連続して発生するイベントに debounce 処理をかけて API 通信の頻度を抑えることで、パフォーマンスの向上を図ることができます。

本題

弊社サービスの管理画面の開発を行う中で、それまでフロントエンドで行っていた検索処理を、バックエンドの検索用 API を経由して行う形に変更する必要が生まれました。それに合わせて、インクリメンタルサーチ機能が利用される際にバックエンドに送信するリクエスト数を抑制する、debounce 処理を掛ける必要が生まれました。 そこで今回はReact アプリケーションで debounce 処理を扱う際に考慮したことなどについて書いていきます。

今回作りたかったもの

ユーザーが文字入力するとアドレスバーのクエリパラメータが更新されます。それにより検索が実行され検索結果が描画されます。

  • キーワード検索ボックス
  • ユーザーが文字を入力している間は検索の実行 (リクエスト送信) を抑制したい
  • ナビゲーション処理は Atomic Design の Pages に相当するコンポーネントに記述したい。そういったコンポーネントで扱うステートの数はなるべく減らしたい

検討したこと

React アプリケーションで検索処理に掛ける debounce 処理の実装は、Lodash の debounce 関数 (https://lodash.com/docs/4.17.15#debounce) を使って行いました。今回のように処理だけを debounce させたいケースでは、コンポーネントのステート管理をシンプルに保つことができるためです。

他に検討したものとして、useDebounce と useDebouncedValue が挙がりました。

useDebounce

例: react-use の useDebounce https://github.com/streamich/react-use/blob/master/docs/useDebounce.md

useDebounce を用いる場合、ユーザーの入力を反映するものとユーザーが入力中かどうかを示すものの2種類のステートを用意する必要があり、今回のケースでは Lodash の debounce 関数で書くよりもコードが冗長になってしまうことが判明したため、採用しませんでした。

useDebouncedValue

例: Mantine UI の useDebouncedValue https://mantine.dev/hooks/use-debounced-value/

useDebouncedValue はステートを複製した値の更新に debounce 処理を掛けます。これを使って処理に debounce 処理を掛けるには useEffect と組み合わせる必要があり、コンポーネントの内部が複雑になってしまうため採用しませんでした。

既存の React アプリケーションに debounce 処理を組み込むに当たって、ユーザーが入力してから最新の検索結果が描画されるまでの流れを整理し、以下のような実行フローになるよう設計しました。 (以下は実際のフローの簡略版です)

ユーザーが文字入力することでReactアプリが持つ入力値ステートが更新されます。それに合わせてクエリパラメータを更新するsetSearchParamsが実行されます。Reactアプリの中のコンポーネントが再レンダリングされることでユーザーが検索結果を閲覧できるようになります。

実装したもの

以下が実装したものの簡易版になります。文字入力されると、検索 API リクエスト用 Hooks が走るようになっています。また、文字入力されている間は、検索 API リクエストを debounce させるようになっています。

React アプリケーションで debouce 関数を呼び出す場合、上記のサンプルのようにメモ化する必要があります。メモ化をしていないと、コンポーネントのスナップショットが更新 (再レンダリング) される度に debounce 関数内で保持している最後に入力された値がリセットされてしまうため、実質的に debounce 処理が機能しなくなってしまいます。

最後に

この機能を作り始めた当初は、useDebouncedValue を使おうとして、挙動の理解が浅かったため上手くいかず詰まっていました。解決したいものが状態なのか振る舞いなのかを切り分けるのは大事ですね。ありがとうございました。