今回やること
前回は、カテゴリとステータスを選び、
条件に合うデータだけを一覧へ表示できるようにしました。
今回は、さらに検索欄を追加して、
名前とメモに含まれる文字からデータを検索できるようにします。
検索欄へ文字を入力
↓
検索キーワードを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.nameとitem.memoを1つの文字列にまとめ、
検索キーワードと同じように英字を小文字へそろえています。
その文字列に検索キーワードが含まれているかを、
includes()で確認します。
return !keyword || target.includes(keyword);
検索欄が空の場合は
!keywordがtrueになるため、
キーワードでは絞り込みません。
検索欄に文字が入力されている場合は、
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で保存して完成