lamechang-dev

Webフロントエンドエンジニア lamechangのブログ。

【TypeScript】【React】【Recoil】翻訳しながらそれなりに型をつけて細々と進めていくRecoil Tutorial その2 Selector編

f:id:lamechang_dev:20220213113823p:plain

本記事は、Reactの提供元であるFacebook改めMetaが開発中の新しい状態管理ライブラリである Recoil の公式チュートリアルを翻訳しつつ、TypeScriptで型付けをしながら進めていく連載の第二弾です。細々とやっていきます🙄 今回は Selector について。

recoiljs.org

Selector

A selector represents a piece of derived state. You can think of derived state as the output of passing state to a pure function that modifies the given state in some way.

Derived state is a powerful concept because it lets us build dynamic data that depends on other data. In the context of our todo list application, the following are considered derived state:

Filtered todo list: derived from the complete todo list by creating a new list that has certain items filtered out based on some criteria (such as filtering out items that are already completed).

Todo list statistics: derived from the complete todo list by calculating useful attributes of the list, such as the total number of items in the list, the number of completed items, and the percentage of items that are completed.

To implement a filtered todo list, we need to choose a set of filter criteria whose value can be saved in an atom. The filter options we'll use are: "Show All", "Show Completed", and "Show Uncompleted". The default value will be "Show All":


セレクターは、派生状態の一部を表します。派生状態は、特定の状態を何らかの方法で変更する純粋関数に状態を渡す出力と考えることができます。

派生状態は、他のデータに依存する動的データを構築できるため、強力な概念です。 ToDoリストアプリケーションのコンテキストでは、以下は派生状態と見なされます。

フィルタリングされたToDoリスト: いくつかの基準に基づいて特定のアイテムが除外された新しいリストを作成することにより、完全なToDoリストから派生します(すでに完了しているアイテムを除外するなど)。

ToDoリストの統計:リスト内のアイテムの総数、完了したアイテムの数、完了したアイテムの割合など、リストの有用な属性を計算することにより、完全なToDoリストから導出されます。

フィルター処理されたToDoリストを実装するには、値をアトムに保存できるフィルター基準のセットを選択する必要があります。使用するフィルターオプションは、「すべて表示」、「完了済みを表示」、「未完了を表示」です。デフォルト値は「すべて表示」になります。

「すべて表示」、「完了済みを表示」、「未完了を表示」がフィルターオプションになるとのことなので、それらの条件を文字列リテラルのユニオンで定義してあげようと思います。すると、コードは以下のようになるかと思います。

export type TodoListFilterType =
  | "Show All"
  | "Show Completed"
  | "Show Uncompleted";

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

 

Using todoListFilterState and todoListState, we can build a filteredTodoListState selector which derives a filtered list:


todoListFilterStateとtodoListStateを使用して、フィルターされたリストを導出するfilteredTodoListState Selectorを構築できます。

selectorは以下のようなコードになります。ここで前回のatomの時と同様に、selector が保持する型を明らかにするためにジェネリクスを使っています。

またtodoListFilterStateの型を文字列リテラルで指定したため、switch文で利用しているcaseに意図しない文字列リテラルが含まれるとエラーを吐くようになることで安全に記述できています。

const filteredTodoListState = selector<Todo[]>({
  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;
    }
  },
});

 

The filteredTodoListState internally keeps track of two dependencies: todoListFilterState and todoListState so that it re-runs if either of those change.


filteredTodoListStateは、todoListFilterStateとtodoListStateの2つの依存関係を内部的に追跡し、これらのいずれかが変更された場合に再実行されるようにします。

 

From a component's point of view, selectors can be read using the same hooks that are used to read atoms. However it's important to note that certain hooks only work with writable state (i.e useRecoilState()). All atoms are writable state, but only some selectors are considered writable state (selectors that have both a get and set property). See the Core Concepts page for more information on this topic.


Componentの観点からは、Atomの読み取りに使用されるのと同じフックを使用してセレクターを読み取ることができます。ただし、特定のフックは書き込み可能な状態(つまり、useRecoilState())でのみ機能することに注意することが重要です。すべてのアトムは書き込み可能な状態ですが、一部のSelectorのみが書き込み可能な状態と見なされます(getプロパティとsetプロパティの両方を持つセレクター)。このトピックの詳細については、「コアコンセプト」ページを参照してください。

ここの部分、つまるところSelector には2通りの形式があって、一部は書き込み可能な状態と扱われてそれ以外はそうならない、ということになります。これは selector()の型定義を見ると分かりやすかったです。以下のように2種類の型定義が存在し、RecoilValueReadOnly<T> を返すケース(getプロパティのみを持つケース)が今回定義したtodoListFilterState に該当します。

export function selector<T>(options: ReadWriteSelectorOptions<T>): RecoilState<T>;
export function selector<T>(options: ReadOnlySelectorOptions<T>): RecoilValueReadOnly<T>;

 

Displaying our filtered todoList is as simple as changing one line in the TodoList component:


フィルタリングされたtodoListの表示は、TodoListコンポーネントの1行を変更するのと同じくらい簡単です。

ふむふむ、といった感じです。Vueを書いたことがある人なら、selectorのコンセプトはVueのcomputedにそっくりだと感じると思います。

const TodoList: React.VFC = () => {
  // changed from todoListState to filteredTodoListState
  const todoList = useRecoilValue(filteredTodoListState);

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

 

Note the UI is showing every todo because todoListFilterState was given a default value of "Show All". In order to change the filter, we need to implement the TodoListFilters component:


todoListFilterStateにはデフォルト値の「ShowAll」が指定されているため、UIにはすべてのtodoが表示されていることに注意してください。フィルタを変更するには、TodoListFiltersコンポーネントを実装する必要があります。

const TodoListFilters: React.VFC = () => {
  const [filter, setFilter] = useRecoilState(todoListFilterState);

  const updateFilter = (e: ChangeEvent<HTMLSelectElement>) => {
    setFilter(e.target.value as TodoListFilterType);
  };

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

ここまでの実装で、以下のような画面になります。フィルタリング機能が追加されましたね。

f:id:lamechang_dev:20220205181238p:plain



With a few lines of code we've managed to implement filtering! We'll use the same concepts to implement the TodoListStats component.

We want to display the following stats:

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

While we could create a selector for each of the stats, an easier approach would be to create one selector that returns an object containing the data we need. We'll call this selector todoListStatsState:


いくつかのコードを使用して、フィルタリングを実装することができました! ToDoListStatsコンポーネントを実装するために同じ概念を使用します。

次の統計を表示したいと思います。

・TODOアイテムの総数
・完成したアイテムの総数
・完成していないアイテムの総数
・完了したアイテムの割合

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

ここは特筆すべきところはないですが、こういった類の統計値を使いたい場合は、依存先が共通であるのであれば同じ selector の中でまとめて返してあげることもできるよ、といったところですかね・・

const todoListStatsState = selector<{
  totalNum: number;
  totalCompletedNum: number;
  totalUncompletedNum: number;
  percentCompleted: number;
}>({
  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,
    };
  },
});


To read the value of todoListStatsState, we use useRecoilValue() once again:


TodoListStatSstateの値を読むには、userecoilValue()をもう一度使用します。

const TodoListStats: React.VFC = () => {
  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リストは以下のようになります。

f:id:lamechang_dev:20220205181605p:plain

ここまでの実装はまさにRecoilのコアコンセプトといったところでしょうか。Reduxなどと比べるとそもそものコード記述量も少なく、かつ簡潔に記述できているな、といったのが率直な印象です。

さて、次回は Asynchronous Data Queries について書こうかと思います。