📚

Recoil(ドキュメント和訳)

2022/01/10に公開

Recoilのドキュメントを和訳しました。
https://recoiljs.org/docs/introduction/getting-started
誤りを見つけましたら、ご教示頂けると幸いです。

Getting Started

Create React App

RecoilはReact用の状態管理ライブラリなので、Recoilを使うにはReactがインストールされ、動作している必要があります。Reactアプリケーションを起動するための最も簡単で推奨される方法は、Create React Appを使うことです。

npx create-react-app my-app

npx は npm 5.2+ 以降に付属するパッケージランナーツールです。古いバージョンの npm の場合は説明を参照してください。
Create React Appのその他のインストール方法については、公式ドキュメントを参照してください。

Installation

Recoilのパッケージはnpmにあります。最新の安定版をインストールするには、以下のコマンドを実行します。

npm install recoil

yarnを使用する場合

yarn add recoil

bowerを使用する場合

bower install --save recoil

RecoilRoot

Recoilを使用するには、当該コンポーネントの親ツリーのどこかにRecoilRootを置く必要があります。RecoilRootを置くのに良い場所は、ルートコンポーネントです。

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

次節では、CharacterCounterコンポーネントを実装します。

Atom

Atomは一つの状態を表します。Atomはどのコンポーネントからでも読み書きが可能です。Atomの値を読み取るコンポーネントは、自動的にそのAtomを取り入れているので、Atomを更新すると、そのAtomを取り入れているすべてのコンポーネントは再レンダリングされます。

const textState = atom({
  key: 'textState', // 他のatoms/selectorsに使用していない一意のID
  default: '', // デフォルト値 (初期値)
});

Atomからの読み取りやAtomへの書き込みが必要なコンポーネントでは、以下のようにuseRecoilState()を使用します。

function CharacterCounter() {
  return (
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  );
}

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
}

Selector

Selectorは、派生したStateを表します。派生したStateとは、Stateを変換したものです。派生したStateは、与えられたStateを何らかの方法で変更する純粋な関数にStateを渡したときの出力と考えることができます。

const charCountState = selector({
  key: 'charCountState', // 他のAtom、Selectorに対して一意のID
  get: ({get}) => {
    const text = get(textState);
    return text.length;
  },
});

useRecoilValueフックを使って、charCountStateの値を読み取ることができます。

function CharacterCount() {
  const count = useRecoilValue(charCountState);
  return <>Character Count: {count}</>;
}

Intro

ここでは、RecoilとReactがインストールされていることを前提に説明します。RecoilとReactの導入方法については、Getting Startedのページを参照してください。
このチュートリアルでは、シンプルなTodoリスト・アプリケーションを作成します。このアプリでは、次のようなことができるようになります。

  • ToDo項目を追加する
  • ToDo項目を編集する
  • ToDo項目を削除する
  • ToDoアイテムのフィルタリング
  • 便利な統計情報を表示する
    途中で、Atom、Selector、Atom Family、Recoil APIで公開されるフックについて説明します。また、最適化についても説明します。

Atom

Atomには、アプリケーションのstateを表す情報源が含まれています。ToDoリストでは、情報源はオブジェクトの配列になり、各オブジェクトはTodoアイテムを表します。
このリストをtodoListState Atomと呼び、atom() 関数で作成します。

const todoListState = atom({
  key: 'todoListState',
  default: [],
});

このAtomには一意なキーを与え、デフォルト値として空の配列を設定します。
このAtomの内容を読み取るには、TodoList コンポーネントの useRecoilValue() フックを使用します。

function TodoList() {
  const todoList = useRecoilValue(todoListState);
  return (
    <>
      {/* <TodoListStats /> */}
      {/* <TodoListFilters /> */}
      <TodoItemCreator />
      {todoList.map((todoItem) => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  );
}

コメントアウトされたコンポーネントについては、この後のセクションで実装します。
新しいTodo項目を作成するには、TodoListStateの内容を更新するセッター関数にアクセスする必要があります。useSetRecoilState() フックを使って、TodoItemCreator コンポーネントのセッター関数を取得することができます。

function TodoItemCreator() {
  const [inputValue, setInputValue] = useState('');
  const setTodoList = useSetRecoilState(todoListState);

  const addItem = () => {
    setTodoList((oldTodoList) => [
      ...oldTodoList,
      {
        id: getId(),
        text: inputValue,
        isComplete: false,
      },
    ]);
    setInputValue('');
  };

  const onChange = ({target: {value}}) => {
    setInputValue(value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button onClick={addItem}>Add</button>
    </div>
  );
}

// utility for creating unique Id
let id = 0;
function getId() {
  return id++;
}

古いTodoリストを元に新しいTodoリストを作成するために、セッター関数のアップデータ形式を使用していることに注意してください。

TodoItemコンポーネントにより、Todoアイテムの値を表示しながら、そのテキストを変更したり、アイテムを削除したりすることができるようになります。
useRecoilState() で todoListState を読み込み、セッター関数を取得して、アイテムのテキストの更新、完了のマーク、削除に使用しています。

function TodoItem({item}) {
  const [todoList, setTodoList] = useRecoilState(todoListState);
  const index = todoList.findIndex((listItem) => listItem === item);

  const editItemText = ({target: {value}}) => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      text: value,
    });

    setTodoList(newList);
  };

  const toggleItemCompletion = () => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      isComplete: !item.isComplete,
    });

    setTodoList(newList);
  };

  const deleteItem = () => {
    const newList = removeItemAtIndex(todoList, index);

    setTodoList(newList);
  };

  return (
    <div>
      <input type="text" value={item.text} onChange={editItemText} />
      <input
        type="checkbox"
        checked={item.isComplete}
        onChange={toggleItemCompletion}
      />
      <button onClick={deleteItem}>X</button>
    </div>
  );
}

function replaceItemAtIndex(arr, index, newValue) {
  return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
}

function removeItemAtIndex(arr, index) {
  return [...arr.slice(0, index), ...arr.slice(index + 1)];
}

これで完全に機能するTodoリストが完成しました!
次のセクションでは、セレクタを使用してリストを次のレベルに引き上げる方法について説明します。

Selectors

Selectorは、派生したStateを表します。派生Stateは、与えられた状態を何らかの方法で変更する純粋な関数に状態を渡したときの出力と考えることができます。

派生Stateは、他のデータに依存する動的なデータを構築することができるため、強力な概念です。今回のTodoリストアプリケーションにおいては、以下のものが派生Stateであると考えられます。

  • フィルタリングされたTodoリスト:完全なTodoリストから、ある基準に基づいて特定のアイテムをフィルタリングした新しいリストを作成することで得られる(すでに完了したアイテムをフィルタリングするなど)。
  • Todoリストの統計:リスト内のアイテムの総数、完了したアイテムの数、完了したアイテムの割合など、リストの有用な属性を計算することによって、完全なTodoリストから導き出される。

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

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

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

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

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

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

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

function TodoList() {
  // changed from todoListState to filteredTodoListState
  const todoList = useRecoilValue(filteredTodoListState);

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

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

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

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

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

以下のような統計情報を表示させたいと考えています。

  • ToDoアイテムの総数
  • 完了したアイテムの総数
  • 未完了アイテムの総数
  • 完了したアイテムの割合

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

const todoListStatsState = selector({
  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,
    };
  },
});

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

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

私たちの要求をすべて満たすTodoリストアプリができました。

  • ToDoアイテムを追加する
  • ToDoアイテムを編集する
  • ToDoアイテムの削除
  • ToDoアイテムのフィルタリング
  • 便利な統計情報を表示する

Discussion