Reactでリアルタイムの検索機能を実装する


個人ブログサイトを作った際、記事一覧画面にキーワード検索やカテゴリーで絞り込む機能を実装しました。

APIから取得したコンテンツなどフロントエンドで保持しているデータに対して絞り込みを行う機能は色んな場面で使えるかなと思うので、そのロジックと具体的な実装方法をまとめておきます。

## 基本的なロジック

データの絞り込みを行う基本的なロジックは、JavaScriptの組み込み関数の`fitler`を利用した**配列操作**です。

> **`filter()`** メソッドは、与えられた関数によって実装されたテストに合格したすべての配列からなる**新しい配列を生成**します。
> 

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

```jsx
["りんご", "みかん", "いちご"].filter((text) => {
  return text !== "りんご";
});

// #=> ["みかん", "いちご"];
```

filterメソッドを使ってReactで検索機能を実装する流れは以下の通りです。

1. ユーザーから検索条件を受け取る。（キーワードの入力、カテゴリーの選択など）
2. 全てのデータが入った配列に対して、filterメソッドにより1.で受け取った検索条件に合致するデータのみを選別して新しい配列を生成する。
3. 2.で生成された配列の中身を画面に一覧表示する。

## 検索機能の実装例

以下のURLはこれから説明する検索機能を実装したサンプルコードです。

サンプルコードにある、`①カテゴリー選択ボタンによる検索`と`②フリーキーワード検索`について解説していきます。

https://codesandbox.io/s/react-serch-c93jo?file=/src/App.js

実際のWebサービスでは、ブログ記事のデータはAPIから受け取ることが多いかと思いますが、今回は予め `posts` という配列で記事データを用意し、フロント側で保持しておきます。

【検索対象とする記事データ】

```jsx
const posts = [
  {
    title: 'useStateの使い方',
    category: 'React'
  },
  {
    title: 'LaravelのMVCモデルについて',
    category: 'Laravel'
  },
  {
    title: '同一オリジンポリシーとCORS',
    category: 'Web'
  },
  {
    title: 'useEffectの使い方',
    category: 'React'
  }
]
```

そして、画面に表示する記事データを管理するための状態変数 `showPosts` を用意し、`showPosts`の記事を一覧表示します。
`showPosts`の初期値には、上記の`posts`を入れておきます。


```jsx
export default function App() {
  const [showPosts, setShowPosts] = useState(posts);

  return (
    <div className="App">
      <h1>記事一覧</h1>
      {showPosts.map((post, index) => {
        return (
          <div key={post.title}>
            <p>{index+1}. {post.title}</p>
            <p>category：{post.category}</p>
          </div>
        );
      })}
    </div>
  );
}
```

ブラウザに表示すると以下の通りです。
![スクリーンショット 2022-01-11 20.59.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1208065/8c7ee64a-f374-450e-9f82-76a65e0ebd67.png)
この記事一覧に対して絞り込みを行っていきます。


## ①カテゴリー選択ボタンによる検索

まずは、カテゴリー選択ボタンでカテゴリーを絞り込む機能を実装してみます。

### 実装手順

1. カテゴリーリストの配列を作成
2. カテゴリー選択ボタンを設置
3. `onClick`時に実行して絞り込み処理を行う`selectCategory`メソッドを定義

実装したコードはこちらです。

```jsx
export default function App() {

  const [showPosts, setShowPosts] = useState(posts);

  // カテゴリーリスト
  const categories = Array.from(new Set(posts.map((post) => post.category)));
	
  // カテゴリー絞り込み
  const selectCategory = (category) => {
    // allを選択した場合は早期return
    if (category === "all") {
      setShowPosts(posts);
      return;
    }

    const selectedPosts = posts.filter((post) => post.category === category);
    setShowPosts(selectedPosts);
  };
  

  return (
    <div className="App">
      <h1>記事一覧</h1>

      {/* カテゴリー選択ボタン */}
      <div>
        <h4>Category：</h4>
        <button onClick={() => selectCategory("all")}>All</button>
        {categories.map((category) => (
            <button onClick={() => selectCategory(category)}>{category}</button>
          ))}
      </div>

      {showPosts.map((post, index) => {
        return (
          <div key={post.title}>
            <p>
              {index + 1}. {post.title}
            </p>
            <p>category：{post.category}</p>
          </div>
        );
      })}
    </div>
  );
}
```

### ポイント解説

ひとつずつ解説します。

**1.カテゴリーリストの配列を作成**
`posts`配列に記事が存在するカテゴリーの分だけカテゴリー選択ボタンを設置したいので、まずはカテゴリーのリストを作成します。

```jsx
// カテゴリーリスト
const categories = Array.from(new Set(posts.map((post) => post.category)));
```

JavaScriptの`Setオブジェクト`を使って、`posts`に存在するカテゴリーを**重複排除**して格納したカテゴリーリストを作っています。

Setオブジェクトについては以下の記事を参考にしました。

https://qiita.com/netebakari/items/7c1db0b0cea14a3d4419

SetオブジェクトはES2015から導入された機能で、重複した値が無いことを保証してくれます。

> `Set` オブジェクトは値のコレクションです。挿入順に要素を反復することができます。`Set` に**重複する値は格納出来ません**。`Set` 内の値はコレクション内で一意となります。
> 

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

**2.　カテゴリー選択ボタンを設置**

各カテゴリーを選択するボタンと、全ての記事を表示するためのAllボタンを作ります。

1.で作成したカテゴリーリストの分だけ、カテゴリー選択ボタンを設置します。

カテゴリー選択ボタンとAllボタンの`onClick`時には`selectCategory`メソッドを実行し、引数には該当のカテゴリーを渡しています。

```jsx
<button onClick={() => selectCategory("all")}>All</button>
{categories.map((category) => (
    <button onClick={() => selectCategory(category)}>{category}</button>
))}
```

**3.　onClick時に実行するselectCategoryメソッドを定義**

`posts`の中から、選択されたカテゴリーと一致するカテゴリーの記事のみを`filter`メソッドで選別しています。

絞り込み後の配列を、一覧表示に使う状態変数`showPosts`にセットしています。

```jsx
const selectCategory = (category) => {
  // allの場合は早期return
  if (category === "all") {
      setShowPosts(posts);
      return;
  }
  const selectedPosts = posts.filter((post) => post.category === category);
  setShowPosts(selectedPosts);
}
```

## ②フリーキーワード検索

次に、ユーザーの入力値による絞り込み（フリーキーワード検索）の機能を実装してみます。

### 実装手順

1. 検索フォームへの入力値を保持する状態変数`inputValue`を定義
2. フリーキーワード検索フォームを設置
3. 検索フォームの`onChange`時に実行する`handleInputChange`メソッドを定義
4. 検索フォームへの入力値が変わる度に実行して絞り込みを行う`search`メソッドを定義

```jsx
export default function App() {
  const [showPosts, setShowPosts] = useState(posts)
  const [inputValue, setInputValue] = useState()

  // 検索欄への入力値をハンドリング
  const handleInputChange = (e) => {
    setInputValue(e.target.value)
    search(e.target.value)
  }

  // 検索欄への入力値での絞り込み
  const search = (value) => {
    // 検索欄への入力が空の場合は早期return
    if (value === "") {
      setShowPosts(posts);
      return;
    }

    const serchedPosts = posts.filter(
      (post) =>
        Object.values(post).filter(
          (item) =>
            item !== undefined &&
            item !== null &&
            item.toUpperCase().indexOf(value.toUpperCase()) !== -1
        ).length > 0
    );

    setShowPosts(serchedPosts);
  }

  return (
    <div className="App">
      <h1>記事一覧</h1>

      {/* フリーキーワード検索フォーム */}
      <div>
          <h4>Search</h4>
          <input type="text" value={inputValue} onChange={handleInputChange} />
      </div>

      {/* 記事一覧表示 */}
      {showPosts.map((post, index) => {
        return (
          <div key={post.title}>
            <p>
              {index + 1}. {post.title}
            </p>
            <p>category：{post.category}</p>
          </div>
        );
      })}
    </div>
  );
}
```

### ポイント解説

大体はReactの基本の知識でカバーできるかと思います。

4.の絞り込みを行うロジックの部分は一見ややこしいですが、一つずつ読み解いて解説していきます。

記事の絞り込みを行っているのは以下の部分です。

```jsx
const serchedPosts = posts.filter(
  (post) =>
    Object.values(post).filter(
      (item) =>
        item !== undefined &&
        item !== null &&
        item.toUpperCase().indexOf(value.toUpperCase()) !== -1
    ).length > 0
)
```

このコードでは、

- タイトルもしくはカテゴリーの**いずれか**にユーザーの入力値を**含む**（完全一致ではない）
- 大文字小文字の違いは許容

という要件で絞り込んでいます。

要点を解説していきます。


- **Object.values()**

```jsx
Object.values(post)
```

上記の部分で、オブジェクトの**プロパティの値を配列で取得**する`Object.values()`メソッドを使い、検索対象の値を格納した配列を作ります。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/values

- **undefinedとnullを除外**

```jsx
item !== undefined &&
item !== null &&
```

次に出てくる`toUpperCase()`は`null`や`undefined`の値に対しては使えません。

なので仮にこの2行が無いと、`Obect.values(post)`で生成された配列の中に`undefined`もしくは`null`があった場合 `Cannot read properties of null (reading 'toUpperCase')` というエラーが出ます。

- **toUpperCase()**

> **`toUpperCase()`** メソッドは、呼び出す文字列の値を（文字列でない場合、文字列に変換して）大文字に変換して返します。
> 

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

**記事のタイトルとカテゴリー**と、**ユーザーの入力値**の双方を大文字に変換することで、**大文字・小文字の違いは許容**します。

- **indexOf()**

> **`indexOf()`** メソッドは、呼び出す `[String](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String)` オブジェクト中で、 `fromIndex` から検索を始め、指定された値が最初に現れたインデックスを返します。値が見つからない場合は  `-1`を返します。
> 

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

```jsx
item.toUpperCase().indexOf(value.toUpperCase()) !== -1
```

値が見つからなければ `-1` を返すという特性を使って、記事のタイトルやカテゴリーにユーザーの入力値を含んでいれば `true` 、含まなければ `false` を返すようにしています。

以上のステップを経て、以下のコードで検索キーワードを含む値が1つでもある記事は `.length > 0` の結果 *true*が返り、`serchedPosts`に格納されます。

```jsx
Object.values(member).filter(
  (item: string) =>
    item !== undefined &&
    item !== null &&
    item.toUpperCase().indexOf(value.toUpperCase()) !== -1
).length > 0
```

あとはこの`serchedPosts`を画面に表示するだけです。

## さいごに
Setオブジェクト、fileterメソッド、indexOfメソッド、toUpperCaseメソッドなど、JavaScriptの持つメソッドについても勉強になりました。

組み込み関数を一通りチラッとでも知っていれば、何かを実装したい時にあれを使えばできそう…って考えられると思うので、元々用意されている関数なんかの知識は増やしていきたいなと思います。


## 参考記事

https://himenon.github.io/docs/javascript/simple-react-local-search-form/

https://qiita.com/takf-jp/items/af10bc05428b1182ece5