React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 02 ー selectorによるフィルタリングと集計


* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 01 ー atomを使った項目操作](https://qiita.com/FumioNonaka/items/3ed6b562a20f6d4abb2b)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 03 ー 構成とロジックを整える](https://qiita.com/FumioNonaka/items/dd0c988bae47ab82e012)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 04 ー 状態を直接exportしないようにする](https://qiita.com/FumioNonaka/items/7368de737a8164bce3b1)**」


「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる」第2回は、`selector`を使います。複数の`atom`から参照した値に手を加えて返すのが`selector`です。他の`selector`からも値が得られます。派生の状態をつくる純粋な関数です。今回のでき上がりは、つぎのサンプル001でCodeSandboxに公開しました。

#### サンプル001■React + TypeScript: Recoil tutorial example 02
https://codesandbox.io/embed/react-typescript-recoil-tutorial-example-02-new-mr5zlt?fontsize=14&hidenavigation=1&theme=dark


# 項目をフィルタリングする
まず加えるのは、Todo項目のフィルタリングです。全件/処理済み/未処理の3つのリスト表示を切り替えられるようにします。そのために新たに定めるのが、つぎのコード001の`atom`(`todoListFilterState`)です。

#### コード001■選択したフィルタを状態としてもつ`atom`
```react:src/todoListFilterState.ts
import { atom } from 'recoil';

export const todoListFilterState = atom<string>({
	key: 'todoListFilterState',
	default: 'Show All',
});
```

以下のコード002で、[`selector()`](https://recoiljs.org/docs/api-reference/core/selector)の引数として渡すオプションオブジェクトのプロパティ`get`に定めたコールバックは、状態の値にもとづいて処理した結果を返します。コールバックの引数から取り出す`get()`が値を参照するための関数です。なお、オプションオブジェクトには、`atom`と同じく、一意の識別子である`key`プロパティを与えてください。

`selector`(`filteredTodoListState `)は、フィルタ切り替えの`atom`(`todoListFilterState`)がもつ3つの値に応じて、Todoリストの配列データ(`todoListState`)から表示すべき項目を[`Array.prototype.filter()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)メソッドで抜き出して返します(デフォルトの`Show All`のときは全件です)。

#### コード002■Todoリストの項目をフィルタリングする`selector`
```react:src/filteredTodoListState.ts
import { selector } from 'recoil';
import { todoListFilterState } from './todoListFilterState';
import { todoListState } from '../state/todoListState';
import type { TodoItemType } from '../state/todoListState';

export const filteredTodoListState = selector<TodoItemType[]>({
	key: 'filteredTodoListState',
	get: ({ get }) => {
		const filter = get(todoListFilterState);
		const list = get(todoListState);
		switch (filter) {
			case 'Show Completed':
				return list.filter((item) => item.isComplete);
			case 'Show Uncompleted':
				return list.filter((item) => !item.isComplete);
			default:
				return list;
		}
	},
});
```

さて、親コンポーネント`TodoList`が表示するのは、つねに全件のデータ(`todoListState`)ではなくなりました。フィルタリングされた項目のリスト(`filteredTodoListState`)です。そこで、読み込む状態はつぎのように差し替えてください。戻り値のJSXに加えたのは、このあとに定める表示項目を切り替えるコンポーネント(`TodoListFilters`)です。

```react:src/TodoList.tsx
// import { todoListState } from './todoListState';
import { filteredTodoListState } from './filteredTodoListState';
import { TodoListFilters } from './TodoListFilters';

export const TodoList: FC = () => {
	// const todoList = useRecoilValue(todoListState);
	const todoList = useRecoilValue(filteredTodoListState);
	return (
		<>
			<TodoListFilters />

		</>
	);
};
```

表示切り替えの`TodoListFilters`は、3項目のプルダウンのコンポーネントです(コード003)。`atom`(`todoListFilterState`)の値をメニューから選択します。状態の読み書きに用いるのは[`useRecoilState()`](https://recoiljs.org/docs/api-reference/core/useRecoilState/)フックです。戻り値の配列の第1要素が状態値の参照で、第2要素の関数を使って設定します。

#### コード003■フィルタを切り替えるプルダウンメニューのコンポーネント
```react:src/TodoListFilters.tsx
import { useCallback } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import { useRecoilState } from 'recoil';
import { todoListFilterState } from './todoListFilterState';

export const TodoListFilters: FC = () => {
	const [filter, setFilter] = useRecoilState(todoListFilterState);
	const updateFilter: ChangeEventHandler<HTMLSelectElement> = useCallback(
		({ target: { value } }) => {
			setFilter(value);
		},
		[setFilter]
	);
	return (
		<>
			Filter:
			<select value={filter} onChange={updateFilter}>
				<option value="Show All">All</option>
				<option value="Show Completed">Completed</option>
				<option value="Show Uncompleted">Uncompleted</option>
			</select>
		</>
	);
};
```

これで、プルダウンメニューで、表示する項目を全件/処理済み/未処理のリストに切り替えられるようになりました。

前掲コード002の`selector`(`filteredTodoListState`)は、内部的にふたつの`atom`(`todoListFilterState`と`todoListState`)の状態に依存し、値を参照しています。Recoilは、依存する値が変わったときに`selector`を再実行する仕組みです。


# 項目の状況を数値化して示す
つぎに、Todoリスト項目の状況を数値化して示しましょう。求めるのは、全件/処理済み/未処理それぞれの件数、さらに処理の進捗割合(パーセンテージ)です。その集計の`selector`をつぎのコード004のように定めます。参照する`atom`は`todoListState`ひとつです。得られたTodoリストの配列(`todoList`)から必要な値を計算して返します。`atom`が基本的にひとつの状態値をもつのに対して、`selector`は複数の状態を用いて求めた複数の値が返せるのです。

#### コード004■Todoリスト項目の状況を数値化して返す`selector`
```react:src/todoListStatsState.ts
import { selector } from 'recoil';
import { todoListState } from './todoListState';

type TodoStatsType = {
	totalNum: number;
	totalCompletedNum: number;
	totalUncompletedNum: number;
	percentCompleted: number;
};
export const todoListStatsState = selector<TodoStatsType>({
	key: 'todoListStatsState',
	get: ({ get }) => {
		const todoList = get(todoListState);
		const totalNum = todoList.length;
		const totalCompletedNum = todoList.filter((item) => item.isComplete).length;
		const totalUncompletedNum = totalNum - totalCompletedNum;
		const percentCompleted =
			totalNum === 0 ? 0 : (totalCompletedNum / totalNum) * 100;
		return {
			totalNum,
			totalCompletedNum,
			totalUncompletedNum,
			percentCompleted,
		};
	},
});
```

集計結果をリストにして示すのが、つぎのコード005の`TodoListStats`コンポーネントです。`selector`(`todoListStatsState`)の返す値は、`useRecoilValue`フックで取り出しています。

#### コード005■集計結果をリストにして示すコンポーネント
```react:src/TodoListStats.tsx
import type { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { todoListStatsState } from './todoListStatsState';

export const TodoListStats: FC = () => {
	const {
		 totalNum,
		 totalCompletedNum,
		 totalUncompletedNum,
		 percentCompleted
	} = useRecoilValue(todoListStatsState);
	const formattedPercentCompleted = Math.round(percentCompleted);
	return (
		<ul>
			<li>Total items: {totalNum}</li>
			<li>Items completed: {totalCompletedNum}</li>
			<li>Items not completed: {totalUncompletedNum}</li>
			<li>Percent completed: {formattedPercentCompleted}</li>
		</ul>
	);
};
```

フィルタを切り替えるプルダウンメニューに加えて、集計リストのコンポーネントを親のTodoリストに差し込めばRecoil公式チュートリアルの作例と同じ機能が備わります。ただし、公式のコードと異なりモジュール化したうえで、TypeScriptの型定義も採り入れました(コード006)。動きは冒頭に掲げたCodeSandboxのサンプル001「[React + TypeScript: Recoil tutorial example 02](https://codesandbox.io/s/react-typescript-recoil-tutorial-example-02-new-mr5zlt?from-embed
)」でお確かめください。

#### コード006■フィルタリングと項目集計の機能が加えられたTodoリストのコンポーネント
```react:src/TodoList.tsx
import { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { filteredTodoListState } from './filteredTodoListState';
import { TodoItem } from './TodoItem';
import { TodoItemCreator } from './TodoItemCreator';
import { TodoListFilters } from './TodoListFilters';
import { TodoListStats } from './TodoListStats';

export const TodoList: FC = () => {
	const todoList = useRecoilValue(filteredTodoListState);
	return (
		<>
			<TodoListStats />
			<TodoListFilters />
			<TodoItemCreator />
			{todoList.map((todoItem) => (
				<TodoItem key={todoItem.id} item={todoItem} />
			))}
		</>
	);
};
```

* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 01 ー atomを使った項目操作](https://qiita.com/FumioNonaka/items/3ed6b562a20f6d4abb2b)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 03 ー 構成とロジックを整える](https://qiita.com/FumioNonaka/items/dd0c988bae47ab82e012)**」
* 「**[React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 04 ー 状態を直接exportしないようにする](https://qiita.com/FumioNonaka/items/7368de737a8164bce3b1)**」