キーワードで検索しよう

← 第13
Reactアプリ作成(JS編)14 / 15
15回 →

今回やること

前回は、カテゴリとステータスを選び、 条件に合うデータだけを一覧へ表示できるようにしました。

今回は、さらに検索欄を追加して、 名前とメモに含まれる文字からデータを検索できるようにします。

検索欄へ文字を入力
↓
検索キーワードをStateで管理する
↓
名前とメモにキーワードが含まれるか確認する
↓
カテゴリ・ステータスの条件と組み合わせる
↓
条件に合うデータだけを一覧へ表示する

前回作ったvisibleItemsに、 今回はキーワード検索の条件を追加していきます。

スポンサーリンク

検索キーワードをStateで管理する

まず、 src/components/ListScreen.jsxを開きます。

前回作ったカテゴリとステータスのStateに加えて、 検索欄へ入力した文字を管理するStateを追加します。

ListScreenコンポーネントの先頭を、 次のようにしてください。

const [query, setQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("すべて");
const [selectedStatus, setSelectedStatus] = useState("すべて");

queryが現在の検索キーワードを保持するstate変数、 setQueryがその値を更新するための更新関数です。

最初は何も検索していないため、 初期値には空文字の""を指定しています。

検索欄を追加する

次に、一覧画面へ検索欄を追加します。

前回作ったfiltersの中に、 検索欄を追加しましょう。

カテゴリのlabelの前に、 次のコードを追加してください。

<label>
  検索
  <input
    type="search"
    value={query}
    onChange={(event) => setQuery(event.target.value)}
    placeholder="名前やメモから検索"
  />
</label>

value={query}で検索欄の値を query Stateと結び付けています。

文字を入力すると、 onChangeによって query Stateが更新されます。

ファイルを保存してブラウザを確認し、 カテゴリとステータスの前に検索欄が表示されればOKです。

この時点では、文字を入力しても一覧はまだ変わりません。 次に検索条件を追加します。

検索に使うキーワードを整える

検索するときは、 入力されたqueryを少し整えてから使います。

ListScreenコンポーネントの3つのStateの下に、次のコードを追加してください。

const keyword = query.trim().toLocaleLowerCase("ja");

trim()で前後の空白を取り除き、 toLocaleLowerCase("ja")で英字を小文字へそろえています。

たとえば、 " React "と入力した場合、 keyword"react"になります。

検索されるデータ側の英字も同じように小文字へそろえることで、 大文字・小文字の違いを気にせず検索できます。

キーワード検索の条件を追加する

前回は、 カテゴリとステータスの条件から visibleItemsを作りました。

現在のvisibleItemsを、 次のコードに変更してください。

const visibleItems = items
  .filter((item) => {
    const target =
      `${item.name} ${item.memo}`.toLocaleLowerCase("ja");

    return !keyword || target.includes(keyword);
  })
  .filter(
    (item) =>
      selectedCategory === "すべて" ||
      item.category === selectedCategory
  )
  .filter(
    (item) =>
      selectedStatus === "すべて" ||
      item.status === selectedStatus
  );

最初のfilter()が、 今回追加したキーワード検索の条件です。

const target =
  `${item.name} ${item.memo}`.toLocaleLowerCase("ja");

item.nameitem.memoを1つの文字列にまとめ、 検索キーワードと同じように英字を小文字へそろえています。

その文字列に検索キーワードが含まれているかを、 includes()で確認します。

return !keyword || target.includes(keyword);

検索欄が空の場合は !keywordtrueになるため、 キーワードでは絞り込みません。

検索欄に文字が入力されている場合は、 target.includes(keyword)trueになるデータだけを残します。

そのあと前回作ったカテゴリとステータスの filter()が続くため、 3つの条件を組み合わせて一覧を絞り込めます。

ブラウザで確認する

ファイルを保存して、 実際に検索してみましょう。

  • 検索欄が空の場合は、キーワードでは絞り込まれない
  • 名前に含まれる文字を入力すると、該当するデータだけが表示される
  • メモに含まれる文字でも検索できる
  • カテゴリやステータスと組み合わせて絞り込める
  • どの条件にも合わない場合は「条件に合うデータがありません。」と表示される

すべて確認できれば、 キーワード検索は完成です。

検索しても元のitems Stateは変更されず、表示するvisibleItemsだけが変わります。

ReactとJavaScriptを分けて見てみよう

今回登場した内容を、 ReactとJavaScriptに分けて整理します。

今回登場したReactとJavaScriptの要素
種類 今回登場したもの
React State、更新関数、Stateの更新による再レンダリング
JavaScript filter()includes()trim()toLocaleLowerCase()

検索欄の入力値を query Stateとして管理する部分はReactです。

一方、 filter()includes()を使って 検索条件に合うデータを取り出す部分はJavaScriptです。

query Stateが変わると ListScreenコンポーネントが再レンダリングされ、 新しい条件でvisibleItemsが計算されます。

今回のまとめ

  • 一覧画面に検索欄を追加した
  • 検索キーワードをquery Stateで管理した
  • trim()toLocaleLowerCase()で検索キーワードを整えた
  • 名前とメモを検索対象にした
  • includes()でキーワードが含まれているか確認した
  • キーワード・カテゴリ・ステータスの3つの条件を組み合わせた
  • 検索しても元のitems Stateは変更しない

今回の完成コード

今回変更した ListScreen.jsxの完成コードです。

src/components/ListScreen.jsx

import { useState } from "react";

import { categories, statuses } from "../options";

export default function ListScreen({ items, onCreate, onEdit }) {
  const [query, setQuery] = useState("");
  const [selectedCategory, setSelectedCategory] = useState("すべて");
  const [selectedStatus, setSelectedStatus] = useState("すべて");

  const keyword = query.trim().toLocaleLowerCase("ja");

  const visibleItems = items
    .filter((item) => {
      const target =
        `${item.name} ${item.memo}`.toLocaleLowerCase("ja");

      return !keyword || target.includes(keyword);
    })
    .filter(
      (item) =>
        selectedCategory === "すべて" ||
        item.category === selectedCategory
    )
    .filter(
      (item) =>
        selectedStatus === "すべて" ||
        item.status === selectedStatus
    );

  return (
    <main className="list-screen">
      <header className="list-header">
        <h1>気になるものリスト</h1>

        <div className="header-actions">
          <button type="button" onClick={onCreate}>
            新規登録
          </button>
        </div>
      </header>

      <h2 className="screen-title">一覧画面</h2>

      <div className="filters">
        <label>
          検索
          <input
            type="search"
            value={query}
            onChange={(event) => setQuery(event.target.value)}
            placeholder="名前やメモから検索"
          />
        </label>

        <label>
          カテゴリ
          <select
            value={selectedCategory}
            onChange={(event) =>
              setSelectedCategory(event.target.value)
            }
          >
            <option value="すべて">すべて</option>
            {categories.map((value) => (
              <option key={value} value={value}>
                {value}
              </option>
            ))}
          </select>
        </label>

        <label>
          ステータス
          <select
            value={selectedStatus}
            onChange={(event) =>
              setSelectedStatus(event.target.value)
            }
          >
            <option value="すべて">すべて</option>
            {statuses.map((value) => (
              <option key={value} value={value}>
                {value}
              </option>
            ))}
          </select>
        </label>
      </div>

      <table>
        <thead>
          <tr>
            <th>名前</th>
            <th>カテゴリ</th>
            <th>ステータス</th>
            <th>価格</th>
            <th>メモ</th>
            <th>操作</th>
          </tr>
        </thead>

        <tbody>
          {visibleItems.map((item) => (
            <tr key={item.id}>
              <td>{item.name}</td>
              <td>{item.category}</td>
              <td>{item.status}</td>
              <td>{item.price.toLocaleString()}円</td>
              <td>{item.memo}</td>
              <td>
                <button
                  type="button"
                  onClick={() => onEdit(item.id)}
                >
                  編集
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>

      {visibleItems.length === 0 && (
        <p>条件に合うデータがありません。</p>
      )}
    </main>
  );
}

アプリの完成状況

  • ✅ Reactを動かし、JSXを表示する
  • ✅ Stateで画面を切り替える
  • ✅ 1件のデータを表示する
  • ✅ 複数のデータを一覧表示する
  • ✅ コンポーネントとpropsを使う
  • ✅ コンポーネントを別ファイルへ分ける
  • ✅ 新規登録画面を作る
  • ✅ 一覧データをStateで管理する
  • ✅ 編集画面を作る
  • ✅ データを更新する
  • ✅ データを削除する
  • ✅ カテゴリ・ステータスで絞り込む
  • ✅ キーワードで検索する
  • ⬜ localStorageで保存して完成
スポンサーリンク