2
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-04

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

サンプル001■React + TypeScript: Recoil tutorial example 02

項目をフィルタリングする

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

コード001■選択したフィルタを状態としてもつatom

src/todoListFilterState.ts
import { atom } from 'recoil';

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

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

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

コード002■Todoリストの項目をフィルタリングするselector

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)です。

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()フックです。戻り値の配列の第1要素が状態値の参照で、第2要素の関数を使って設定します。

コード003■フィルタを切り替えるプルダウンメニューのコンポーネント

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(todoListFilterStatetodoListState)の状態に依存し、値を参照しています。Recoilは、依存する値が変わったときにselectorを再実行する仕組みです。

項目の状況を数値化して示す

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

コード004■Todoリスト項目の状況を数値化して返すselector

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■集計結果をリストにして示すコンポーネント

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」でお確かめください。

コード006■フィルタリングと項目集計の機能が加えられたTodoリストのコンポーネント

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} />
			))}
		</>
	);
};
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1