AlgoliaのInstantsearchを用いて検索UIを作っていたが、検索文字列に変化があるたびにリクエストを送る仕様になっていた
なのである程度入力が終わったと判断できるタイミングでリクエストを送るようにしたいということで調べて対応してみた
Algoliaの料金体系とInstantsearchの挙動
自分はFreeプランしか使っていないが
10000req/月もしくは10000Record/月までは無料で使える
基本的にAlgoliaを使うのであればUI側はAlgoliaが提供するライブラリInstantsearchを使って開発することになるはず
そして、Instantsearchを使って実装する場合、デフォルトの挙動としては検索文字列に変化があるとそのぶんだけリクエストを送るようになっている
たとえば「Ruby」で検索しようとした場合
Rで検索リクエストRuで検索リクエストRubで検索リクエストRubyで検索リクエスト
というようになる
このような方式をas you typeと呼ぶらしい(ドキュメントなどにそう書いてあることが多い)
この場合に次の2点でちょっと微妙だなと感じていた
- 無料プランだと最終的に検索したいのは「Ruby」なのでその入力が終わるまでリクエスト送信を待ってほしい
- 結果が返ってくるまでに時間がかかる場合、検索結果のレンダリングがカクつく
- 実装が悪い可能性もある…
上記を解決するために「検索文字列の変化が一定時間以上なかったら」検索リクエストを送るようにしてみる
調べたら、ドキュメントに対応方法が載っていたので対応していく
Improve performance for React InstantSearch Hooks | Algolia
https://www.algolia.com/doc/guides/building-search-ui/going-further/improve-performance/react-hooks/#disabling-as-you-typewww.algolia.com
Reactで使うInstantsearchについて
InstantsearchのReact用ライブリは2022-08-03現在2種類あり、旧版のreact-instantsearch-dom、新版のreact-instantsearch-hooks-webがある
react-instantsearch-hooks-webの方は、名前の通りReact Hooksが使えるようになっている
React Instantsearch(旧)のドキュメントを見に行くと「新たに使う場合はhooks版を使ってください」と書いてあったので基本的にはhooksの方を使えば良さそう
今回はアナウンスに従いReact Instantsearch Hooksを使った
検索文字列に1秒変化がない場合に検索リクエストを送信する
ドキュメントの実装例がJavaScriptなのでコピーするだけだと型周りでエラーが出てしまうためそのあたりをドキュメントの実装例からは変更している
- tsx(一部抜粋)
import type { NextPage } from "next";
import algoliasearch from "algoliasearch/lite";
import { useRef } from "react";
import {
InstantSearch,
SearchBox,
Index,
InfiniteHits,
PoweredBy,
} from "react-instantsearch-hooks-web";
import type { UseSearchBoxProps } from "react-instantsearch-hooks-web";
import type { SearchClient } from "instantsearch.js";
const Search: NextPage = () => {
const indices = (process.env.NEXT_PUBLIC_ALGOLIA_INDICES || "").split(",");
const timerId = useRef<ReturnType<typeof setTimeout>>();
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "",
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY || ""
);
// NOTE: https://www.algolia.com/doc/guides/building-search-ui/going-further/improve-performance/react-hooks/#disabling-as-you-type
// 入力確定判断まで1秒待つ
const queryHook: UseSearchBoxProps["queryHook"] = (query, search) => {
if (timerId.current) {
clearTimeout(timerId.current);
}
timerId.current = setTimeout(() => search(query), 1000);
};
return (
<InstantSearch searchClient={searchClient} indexName={indices[1]}>
<SearchBox placeholder="Search" queryHook={queryHook}></SearchBox>
<div>
{indices.map((index) => {
return (
<div key={index}>
<Index key={index} indexName={index}>
<h2>{index}</h2>
<InfiniteHits
hitComponent={PageHit}
></InfiniteHits>
</Index>
</div>
);
})}
</div>
<br />
<PoweredBy />
</InstantSearch>
);
};
InfiniteHitsコンポーネント以下は独自で実装しているが今回の話の範囲ではないため除外する
ポイントはSearchBoxのqueryHookプロパティ
このqueryHookはクエリの文字列に変化があるたびに呼ばれるコールバック関数を渡す
用途としては
- クエリの内容を入力された文字列から少し変えたい
- クエリによって処理を変えたい
など、検索文字列の変化から検索リクエストの間に何かしらの処理を挟みたい場合に使う
ドキュメントではsetTimeoutと組み合わせることで「一定期間入力がない場合のみ検索のリクエストを送信する」ということを行っている
今回やりたいことはまさにこのパターンなのでドキュメントにしたがってやっていく
queryHook関数の引数でqueryは検索文字列、searchは検索の実行(リクエストをAlgoliaに送信する処理)
setTimeoutで1000ms後に検索を実行する、それまでに入力の変化によりコールバックが呼ばれた場合はclearTimeoutで設定されたTimerを解除して新たにTimerをセットするというような実装になっている
型に関しては下記のようにReturnTypeでsetTimeoutの戻り値の型を取得して型注釈としている
const timerId = useRef<ReturnType<typeof setTimeout>>();
単純にuseStateでtimerIdを管理しようとするとうまくいかなかったのでuseRefを使って管理するようにした
こういう使い方をできるのを知らなかったので勉強になった
まとめ
- AlgoliaのInstantsearchで、SearchBoxのqueryHookコールバックを用いて検索文字列の入力が1秒間ない場合に初めてリクエストを送るようにした
- useRefを用いてsetTimeoutの戻り値を管理するようにした
また、感想として、AlgoliaのInstantsearchのドキュメントを眺めてみるとさまざまなユースケースに対応できるようにかなりいろいろ機能があるなーと感じた
とりあえず一通りさっと読んで把握するだけでも面白いと思う