Selector は derived state の一部を表します。derived state とは、純粋な関数に状態を渡して、その状態から新しい値を派生させる出力と考えることができます。React の useMemo 的なもの?

Derived state は、他のデータに依存する動的なデータを構築することができます。今回の Todo リストアプリでは、以下が derived state であると考えられます。

  • Filtered todo list : Todo リストから、すでに完了した項目を除外するなどフィルタリングした新しいリストを作成することにより、派生したものです。
  • Todo list statistics: リスト内の項目の総数、完了した項目の数、完了した項目の割合など、リストの有用な属性を計算することによって、Todoリストから導出されます。

フィルタリングした Todo リストを実装するには、atom に値を保存できるフィルタ条件を選択する必要があります。使用するフィルターオプションは以下の通りです。「すべて表示」「完了表示」「未完了表示」。デフォルト値は「すべて表示」。

export const todoListFilterState = atom({
  key: "TodoListFilter",
  default: "Show All"
});

todoListFilterState と todoListState を使って、フィルタリングさせたリストを派生させる filteredTodoListState セレクタを作ることができます。

import { selector } from "recoil";
import { todoListFilterState, todoListState } from "./atomos";

export const filteredTodoListState = selector({
  key: "FilteredTodoList",
  get: ({ get }) => {
    const filter = get(todoListFilterState);
    const list = get(todoListState);

    switch (filter) {
      case "Show Complete":
        return list.filter((item) => item.isComplete);
      case "Show Uncompleted":
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  }
});

filteredTodoListState は、todoListFilterState と todoListState の2つの依存関係を内部的に記録し、どちらかが変更された場合に再実行します。

コンポーネントから見ると、セレクタはアトムの読み込みに使われるのと同じフックを使って読み込むことができる。しかし、ある種のフックは書き込み可能な状態でのみ動作することに注意する必要があります(例:useRecoilState())。すべてのアトムは書き込み可能な状態ですが、一部のセレクタだけが書き込み可能な状態とみなされます(getとsetの両方のプロパティを持つセレクタ)。このトピックの詳細については、Core Conceptsのページを参照してください。

https://recoiljs.org/docs/basic-tutorial/selectors

TodoList コンポーネントの1行を変更するだけで、フィルタリングされた TodoList を表示することができます。

import { useRecoilValue } from "recoil";
import TodoItemCreator from "./TodoItemCreator";
import TodoItem from "./TodoItem";
import { filteredTodoListState } from "./selector";

function TodoList() {
  const todoList = useRecoilValue(filteredTodoListState);

  return (
    <>
      <TodoListStats />
      <TodoListFilters />
      <TodoItemCreator />

      {todoList.map((todoItem) => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  );
}

export default TodoList;

todoListFilterState のデフォルト値が “Show All” なので、UI にはすべての ToDo が表示されていることに注意してください。フィルタを変更するには、TodoListFiltersコンポーネントを実装する必要があります。

import { useRecoilState } from "recoil";
import { todoListFilterState } from "./atomos";

function TodoListFilters() {
  const [filter, setFilter] = useRecoilState(todoListFilterState);

  const updateFilter = ({ target: { value } }) => {
    setFilter(value);
  };

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

export default TodoListFilters;

数行のコードで、フィルタリングを実装することができました。同じコンセプトで TodoListStats コンポーネントを実装してみましょう。

以下のような統計情報を表示したい。

  • Total number of todo items
  • Total number of completed items
  • Total number of uncompleted items
  • Percentage of items completed

統計情報ごとにセレクタを作成することもできますが、より簡単な方法は、必要なデータを含むオブジェクトを返すセレクタを1つ作成することでしょう。このセレクタを todoListStatsState と呼ぶことにします。

export const todoListStatsState = selector({
  key: "TodoListStats",
  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
    };
  }
});

todoListStatsStateの値を読み取るために、もう一度useRecoilValue()を使用します。

import { useRecoilValue } from "recoil";
import { todoListStatsState } from "./selector";

function TodoListStats() {
  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>
  );
}

export default TodoListStats;

Sample