React Hooks でキーワードによる絞り込み検索を実装する

# やりたいこと
APIから取得しフロントエンド側でプールしているデータを、テキスト入力などのUIからキーワードでリアルタイムに絞り込みを行う、インクリメンタルサーチのようなものを実現したい機会があったので作ってみた。

# つくったもの
サンプルコード：https://codesandbox.io/s/filtering-search-demo-btjgn
React v16.12.0 を使用しています。
UIフレームワークは前回と同じく [Material UI](https://material-ui.com/) でサクっと用意。

# ソースコードとコンポーネントについて

## 検索対象となるデータ
クライアント側で検索したいデータはサーバーサイドから fetch してきたレスポンスに基づいたものや、定数的にもともと保持しているものを使いたいケースが考えられますが、今回はサクっと実例だけを提示したかったため tsx のファイルにとりあえずサンプルとなるものをstring配列として定義しました。

```Search.tsx
const products: string[] = [
  "Apple",
  "Banana",
  "Orange",
  "Cheese Cake",
  "Banana Cake",
  "Apple Juice",
  "Orange Juice"
];
```

## `<SearchTextField />`
検索キーワードの入力を受け付けるUIです。
また、`onClick`時に対象となるデータを List 形式にしてぶら下げて表示させています。

```Search.tsx
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`内で行なっている処理について一つずつ注釈していきます。

```Search.tsx
useEffect(() => {
  if (keyword === "") {
    setFilteredProducts(products);
    return;
  }
```
ここでは一度入力したキーワードを全て消去した時(テキストフィールドの値が空文字`""`になった時)、絞り込み内容をリセットして初期値の products を改めて全件セットしています。同時に List コンポーネントを非表示にしたいパターンもあると思うのでその辺の実装はお好みで。

```Search.tsx
const searchKeywords = keyword
  .trim()
  .toLowerCase()
  .match(/[^\s]+/g);
```
ここでは、入力されたキーワードを検索条件として使用するために配列に変更して格納しています。キーワードの間に空白がある場合、その空白を区切りにした配列を`match()`メソッドを用いて生成しています。この配列は、後述の`every()`メソッドで検索条件として使用します。

入力されたキーワードの両端に空白があるとノイズになり検索の妨げとなるので、`trim()`でそれらを削ったあと、`toLowerCase()`ですべて小文字に変換しています。後述の処理(Array.every)で英字に関しては大文字・小文字の一致もチェックする仕様となっているので、それらの区別なくキーワードで絞り込みを行いたい時は`toLowerCase`での変換が必須となります。
 [MDN - Array.prototype.every()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/every) 


また、`match`の中に記述している`/[^\s]+/g`という正規表現についてですが、`[^\s]+`は空白文字(半角・全角)以外の任意の一文字以上の連続にヒットさせる条件です。
`g`フラグは`match`メソッドのオプションで、文字列の中から正規表現に一致する要素を全て配列として返却させることができます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/match

```Search.tsx
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