- 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 01 ー atomを使った項目操作」
- 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 03 ー 構成とロジックを整える」
- 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 04 ー 状態を直接exportしないようにする」
「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
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
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
)です。
// 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■フィルタを切り替えるプルダウンメニューのコンポーネント
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
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■集計結果をリストにして示すコンポーネント
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リストのコンポーネント
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} />
))}
</>
);
};