やりたいこと
APIから取得しフロントエンド側でプールしているデータを、テキスト入力などのUIからキーワードでリアルタイムに絞り込みを行う、インクリメンタルサーチのようなものを実現したい機会があったので作ってみた。
つくったもの
サンプルコード:https://codesandbox.io/s/filtering-search-demo-btjgn
React v16.12.0 を使用しています。
UIフレームワークは前回と同じく Material UI でサクっと用意。
ソースコードとコンポーネントについて
検索対象となるデータ
クライアント側で検索したいデータはサーバーサイドから fetch してきたレスポンスに基づいたものや、定数的にもともと保持しているものを使いたいケースが考えられますが、今回はサクっと実例だけを提示したかったため tsx のファイルにとりあえずサンプルとなるものをstring配列として定義しました。
const products: string[] = [
"Apple",
"Banana",
"Orange",
"Cheese Cake",
"Banana Cake",
"Apple Juice",
"Orange Juice"
];
<SearchTextField />
検索キーワードの入力を受け付けるUIです。
また、onClick
時に対象となるデータを List 形式にしてぶら下げて表示させています。
const SearchTextField: React.FC = () => {
// 入力キーワード
const [keyword, setKeyword] = useState("");
// itemsのListを表示・非表示を切替。onClick で true を渡して表示させる
const [showLists, setShowLists] = useState(false);
// List 形式で表示するデータ。初期値では検索キーワードを入力していないので上で定義した
// products を全件渡している
const [filteredProducts, setFilteredProducts] = useState(products);
絞り込み検索のロジック
今回はuseEffect
を用いて、keyword
が入力される毎にproducts
の中からそれに合致する要素を抜き出して新たに配列を生成し、filteredItems
に格納しています。
useEffect
内で行なっている処理について一つずつ注釈していきます。
useEffect(() => {
if (keyword === "") {
setFilteredProducts(products);
return;
}
ここでは一度入力したキーワードを全て消去した時(テキストフィールドの値が空文字""
になった時)、絞り込み内容をリセットして初期値の products を改めて全件セットしています。同時に List コンポーネントを非表示にしたいパターンもあると思うのでその辺の実装はお好みで。
const searchKeywords = keyword
.trim()
.toLowerCase()
.match(/[^\s]+/g);
ここでは、入力されたキーワードを検索条件として使用するために配列に変更して格納しています。キーワードの間に空白がある場合、その空白を区切りにした配列をmatch()
メソッドを用いて生成しています。この配列は、後述のevery()
メソッドで検索条件として使用します。
入力されたキーワードの両端に空白があるとノイズになり検索の妨げとなるので、trim()
でそれらを削ったあと、toLowerCase()
ですべて小文字に変換しています。後述の処理(Array.every)で英字に関しては大文字・小文字の一致もチェックする仕様となっているので、それらの区別なくキーワードで絞り込みを行いたい時はtoLowerCase
での変換が必須となります。
MDN - Array.prototype.every()
また、match
の中に記述している/[^\s]+/g
という正規表現についてですが、[^\s]+
は空白文字(半角・全角)以外の任意の一文字以上の連続にヒットさせる条件です。
g
フラグはmatch
メソッドのオプションで、文字列の中から正規表現に一致する要素を全て配列として返却させることができます。
const result = products.filter((product) =>
searchKeywords.every((kw) => product.toLowerCase().indexOf(kw) !== -1)
);
setFilteredProducts(result.length ? result : ["No Item Found"]);
最後に、入力したキーワード群を文字列として含む結果を絞り込む処理について解説します。
filter()
メソッドは配列の各要素を任意の関数(boolean を返り値とするもの)にわたし、true
となるものを絞り込み、新たに配列を生成します。
以下はfilter
メソッドが各要素を渡す関数です。
searchKeywords.every((kw) => product.toLowerCase().indexOf(kw) !== -1)
searchKeywords
には検索条件として入力されたキーワードが格納されています。それらをevery()
メソッドで一つずつ取り出し、 product
内に文字として含まれているか突き合わせ、indexOf()
で精査しています。
検索キーワードがproduct
の文字列に含まれていると値として-1
が返ってこないため、上記の関数の返り値は"true" となり、対象となるproduct
がresult
内の配列に格納されます。indexOf()
で精査することによってキーワードとデータの文字列が部分一致している場合でも絞り込みと抽出が可能となっています。
大文字・小文字の区別なくヒットさせるため、searchKeywords
とproduct
をともにtoLowerCase()
で小文字に一律置き換えているのは前述のとおりです。
最後に
冒頭でもURLを記載しましたが、下記のサンプルコードから色々データを追加して動作確認などお試しください。
https://codesandbox.io/s/filtering-search-demo-btjgn